Skip to content

Add ThreadContextStack injection capability #3810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,58 @@ void testContainsKey() {
assertFalse(ThreadContext.containsKey("testKey"));
}

@Test
void testStackBasicOperations() {
ThreadContext.clearStack();
assertEquals(0, ThreadContext.getDepth(), "Stack should be empty initially");

ThreadContext.push("first");
assertEquals(1, ThreadContext.getDepth(), "Stack depth should be 1");
assertEquals("first", ThreadContext.peek(), "Peek should return last pushed item");

ThreadContext.push("second");
assertEquals(2, ThreadContext.getDepth(), "Stack depth should be 2");
assertEquals("second", ThreadContext.peek(), "Peek should return last pushed item");

assertEquals("second", ThreadContext.pop(), "Pop should return last pushed item");
assertEquals(1, ThreadContext.getDepth(), "Stack depth should be 1 after pop");
assertEquals("first", ThreadContext.peek(), "Peek should return remaining item");

ThreadContext.clearStack();
assertEquals(0, ThreadContext.getDepth(), "Stack should be empty after clear");
}

@Test
void testCustomStackIntegration() {
String originalProperty = System.getProperty("log4j2.threadContextStack");
try {
System.setProperty(
"log4j2.threadContextStack",
"org.apache.logging.log4j.spi.ThreadContextStackFactoryTest$CustomThreadContextStack");
ThreadContext.init();
ThreadContext.push("test");
assertEquals("test", ThreadContext.peek(), "Custom stack should work normally");
} finally {
if (originalProperty != null) {
System.setProperty("log4j2.threadContextStack", originalProperty);
} else {
System.clearProperty("log4j2.threadContextStack");
}
ThreadContext.init();
}
}

@Test
void testClearAll() {
ThreadContext.put("key", "value");
ThreadContext.push("stackItem");
assertFalse(ThreadContext.isEmpty(), "Map should not be empty");
assertEquals(1, ThreadContext.getDepth(), "Stack should not be empty");
ThreadContext.clearAll();
assertTrue(ThreadContext.isEmpty(), "Map should be empty after clearAll");
assertEquals(0, ThreadContext.getDepth(), "Stack should be empty after clearAll");
}

private static class TestThread extends Thread {

private final StringBuilder sb;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.logging.log4j.spi;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.test.junit.SetTestProperty;
import org.apache.logging.log4j.test.junit.UsingAnyThreadContext;
import org.junit.jupiter.api.Test;

@UsingAnyThreadContext
class ThreadContextStackFactoryTest {

@Test
void testDefaultThreadContextStack() {
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
assertNotNull(stack, "ThreadContextStack should not be null");
assertTrue(stack instanceof DefaultThreadContextStack, "Should return DefaultThreadContextStack by default");
}

@Test
@SetTestProperty(
key = "log4j2.threadContextStack",
value = "org.apache.logging.log4j.spi.ThreadContextStackFactoryTest$CustomThreadContextStack")
void testCustomThreadContextStack() {
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
assertNotNull(stack, "ThreadContextStack should not be null");
assertTrue(
stack instanceof CustomThreadContextStack,
"Expected CustomThreadContextStack but got " + stack.getClass().getName());

stack.push("test");
assertEquals("test", stack.peek(), "Custom stack should work normally");
}

@Test
@SetTestProperty(
key = "log4j2.threadContextStack",
value = "org.apache.logging.log4j.spi.ThreadContextStackFactoryTest$VerifiableThreadContextStack")
void testCustomStackRealBehavior() {
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
assertTrue(stack instanceof VerifiableThreadContextStack, "Should be VerifiableThreadContextStack");

VerifiableThreadContextStack verifiableStack = (VerifiableThreadContextStack) stack;

stack.push("operation1");
assertEquals("CUSTOM:operation1", stack.peek(), "Push should add custom prefix");
assertEquals(1, verifiableStack.getCallCount(), "Should track method calls");

stack.push("operation2");
assertEquals("CUSTOM:operation2", stack.peek(), "Second push should also have prefix");
assertEquals(2, verifiableStack.getCallCount(), "Call count should increment");

String popped = stack.pop();
assertEquals("CUSTOM:operation2", popped, "Pop should return prefixed value");
assertEquals(3, verifiableStack.getCallCount(), "Pop should increment call count");
assertEquals("CUSTOM:operation1", stack.peek(), "Remaining item should have prefix");

List<String> stackList = stack.asList();
assertEquals(1, stackList.size(), "Should have one remaining item");
assertEquals("CUSTOM:operation1", stackList.get(0), "List should contain prefixed item");
assertEquals(4, verifiableStack.getCallCount(), "asList should increment call count");

assertTrue(verifiableStack.wasMethodCalled("push"), "Should track push calls");
assertTrue(verifiableStack.wasMethodCalled("pop"), "Should track pop calls");
assertTrue(verifiableStack.wasMethodCalled("asList"), "Should track asList calls");
assertFalse(verifiableStack.wasMethodCalled("clear"), "Should not track uncalled methods");

stack.clear();
assertEquals(5, verifiableStack.getCallCount(), "Clear should also be tracked");
assertTrue(verifiableStack.wasMethodCalled("clear"), "Should track clear call");
}

@Test
@SetTestProperty(key = "log4j2.threadContextStack", value = "com.nonexistent.StackClass")
void testInvalidThreadContextStackClass() {
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
assertNotNull(stack, "ThreadContextStack should not be null");
assertTrue(
stack instanceof DefaultThreadContextStack,
"Should fallback to DefaultThreadContextStack when custom class fails to load");
}

@Test
@SetTestProperty(key = "log4j2.disableThreadContextStack", value = "true")
void testDisabledThreadContextStack() {
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
assertNotNull(stack, "ThreadContextStack should not be null");
assertSame(ThreadContext.NOOP_STACK, stack, "Should return NOOP_STACK when disabled");
}

@Test
@SetTestProperty(key = "log4j2.disableThreadContext", value = "true")
void testDisabledThreadContext() {
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
assertNotNull(stack, "ThreadContextStack should not be null");
assertSame(ThreadContext.NOOP_STACK, stack, "Should return NOOP_STACK when ThreadContext is disabled");
}

@Test
void testFactoryInitDoesNotThrow() {
assertDoesNotThrow(() -> ThreadContextStackFactory.init(), "ThreadContextStackFactory.init() should not throw");
}

@Test
void testFactoryCreateReturnsNonNull() {
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
assertNotNull(stack, "createThreadContextStack() should never return null");
}

public static class CustomThreadContextStack extends DefaultThreadContextStack {
public CustomThreadContextStack() {
super();
}

@Override
public String toString() {
return "CustomThreadContextStack";
}
}

public static class VerifiableThreadContextStack extends DefaultThreadContextStack {

private static final String PREFIX = "CUSTOM:";
private int callCount = 0;
private final Set<String> calledMethods = new HashSet<>();

@Override
public void push(String message) {
trackCall("push");
super.push(PREFIX + message);
}

@Override
public String pop() {
trackCall("pop");
return super.pop();
}

@Override
public String peek() {
calledMethods.add("peek");
return super.peek();
}

@Override
public List<String> asList() {
trackCall("asList");
return super.asList();
}

@Override
public void clear() {
trackCall("clear");
super.clear();
}

private void trackCall(String methodName) {
callCount++;
calledMethods.add(methodName);
}

public int getCallCount() {
return callCount;
}

public boolean wasMethodCalled(String methodName) {
return calledMethods.contains(methodName);
}

@Override
public String toString() {
return "VerifiableThreadContextStack[calls=" + callCount + ", methods=" + calledMethods + "]";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.spi.CleanableThreadContextMap;
import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.apache.logging.log4j.spi.DefaultThreadContextStack;
import org.apache.logging.log4j.spi.MutableThreadContextStack;
import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
import org.apache.logging.log4j.spi.ThreadContextMap;
import org.apache.logging.log4j.spi.ThreadContextMap2;
import org.apache.logging.log4j.spi.ThreadContextMapFactory;
import org.apache.logging.log4j.spi.ThreadContextStack;
import org.apache.logging.log4j.spi.ThreadContextStackFactory;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.apache.logging.log4j.util.ProviderUtil;

Expand Down Expand Up @@ -196,6 +196,8 @@ public boolean retainAll(final Collection<?> ignored) {
@SuppressWarnings("PublicStaticCollectionField")
public static final ThreadContextStack EMPTY_STACK = new EmptyThreadContextStack();

public static final ThreadContextStack NOOP_STACK = new NoOpThreadContextStack();

private static final String DISABLE_STACK = "disableThreadContextStack";
private static final String DISABLE_ALL = "disableThreadContext";

Expand All @@ -220,8 +222,8 @@ private ThreadContext() {
public static void init() {
final PropertiesUtil properties = PropertiesUtil.getProperties();
contextStack = properties.getBooleanProperty(DISABLE_STACK) || properties.getBooleanProperty(DISABLE_ALL)
? new NoOpThreadContextStack()
: new DefaultThreadContextStack();
? NOOP_STACK
: ThreadContextStackFactory.createThreadContextStack();
// TODO: Fix the tests that need to reset the thread context map to use separate instance of the
// provider instead.
ThreadContextMapFactory.init();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
* @see <a href="https://logging.apache.org/log4j/2.x/manual/api.html">Log4j 2 API manual</a>
*/
@Export
@Version("2.20.2")
@Version("2.21.0")
package org.apache.logging.log4j;

import org.osgi.annotation.bundle.Export;
Expand Down
Loading