Skip to content

Commit cd6cdd3

Browse files
committed
Add ThreadContextStack injection capability
- Add ThreadContextStackFactory following ThreadContextMapFactory pattern - Extend Provider class with ThreadContextStack support (backward compatible) - Add NOOP_STACK constant to ThreadContext - Support log4j2.threadContextStack system property - Add comprehensive tests - Update package version for BND compliance Fixes #1507
1 parent c5a1955 commit cd6cdd3

File tree

11 files changed

+433
-9
lines changed

11 files changed

+433
-9
lines changed

log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,58 @@ void testContainsKey() {
162162
assertFalse(ThreadContext.containsKey("testKey"));
163163
}
164164

165+
@Test
166+
void testStackBasicOperations() {
167+
ThreadContext.clearStack();
168+
assertEquals(0, ThreadContext.getDepth(), "Stack should be empty initially");
169+
170+
ThreadContext.push("first");
171+
assertEquals(1, ThreadContext.getDepth(), "Stack depth should be 1");
172+
assertEquals("first", ThreadContext.peek(), "Peek should return last pushed item");
173+
174+
ThreadContext.push("second");
175+
assertEquals(2, ThreadContext.getDepth(), "Stack depth should be 2");
176+
assertEquals("second", ThreadContext.peek(), "Peek should return last pushed item");
177+
178+
assertEquals("second", ThreadContext.pop(), "Pop should return last pushed item");
179+
assertEquals(1, ThreadContext.getDepth(), "Stack depth should be 1 after pop");
180+
assertEquals("first", ThreadContext.peek(), "Peek should return remaining item");
181+
182+
ThreadContext.clearStack();
183+
assertEquals(0, ThreadContext.getDepth(), "Stack should be empty after clear");
184+
}
185+
186+
@Test
187+
void testCustomStackIntegration() {
188+
String originalProperty = System.getProperty("log4j2.threadContextStack");
189+
try {
190+
System.setProperty(
191+
"log4j2.threadContextStack",
192+
"org.apache.logging.log4j.spi.ThreadContextStackFactoryTest$CustomThreadContextStack");
193+
ThreadContext.init();
194+
ThreadContext.push("test");
195+
assertEquals("test", ThreadContext.peek(), "Custom stack should work normally");
196+
} finally {
197+
if (originalProperty != null) {
198+
System.setProperty("log4j2.threadContextStack", originalProperty);
199+
} else {
200+
System.clearProperty("log4j2.threadContextStack");
201+
}
202+
ThreadContext.init();
203+
}
204+
}
205+
206+
@Test
207+
void testClearAll() {
208+
ThreadContext.put("key", "value");
209+
ThreadContext.push("stackItem");
210+
assertFalse(ThreadContext.isEmpty(), "Map should not be empty");
211+
assertEquals(1, ThreadContext.getDepth(), "Stack should not be empty");
212+
ThreadContext.clearAll();
213+
assertTrue(ThreadContext.isEmpty(), "Map should be empty after clearAll");
214+
assertEquals(0, ThreadContext.getDepth(), "Stack should be empty after clearAll");
215+
}
216+
165217
private static class TestThread extends Thread {
166218

167219
private final StringBuilder sb;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.spi;
18+
19+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
import static org.junit.jupiter.api.Assertions.assertNotNull;
23+
import static org.junit.jupiter.api.Assertions.assertSame;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
25+
26+
import java.util.HashSet;
27+
import java.util.List;
28+
import java.util.Set;
29+
import org.apache.logging.log4j.ThreadContext;
30+
import org.apache.logging.log4j.test.junit.SetTestProperty;
31+
import org.apache.logging.log4j.test.junit.UsingAnyThreadContext;
32+
import org.junit.jupiter.api.Test;
33+
34+
@UsingAnyThreadContext
35+
class ThreadContextStackFactoryTest {
36+
37+
@Test
38+
void testDefaultThreadContextStack() {
39+
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
40+
assertNotNull(stack, "ThreadContextStack should not be null");
41+
assertTrue(stack instanceof DefaultThreadContextStack, "Should return DefaultThreadContextStack by default");
42+
}
43+
44+
@Test
45+
@SetTestProperty(
46+
key = "log4j2.threadContextStack",
47+
value = "org.apache.logging.log4j.spi.ThreadContextStackFactoryTest$CustomThreadContextStack")
48+
void testCustomThreadContextStack() {
49+
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
50+
assertNotNull(stack, "ThreadContextStack should not be null");
51+
assertTrue(
52+
stack instanceof CustomThreadContextStack,
53+
"Expected CustomThreadContextStack but got " + stack.getClass().getName());
54+
55+
stack.push("test");
56+
assertEquals("test", stack.peek(), "Custom stack should work normally");
57+
}
58+
59+
@Test
60+
@SetTestProperty(
61+
key = "log4j2.threadContextStack",
62+
value = "org.apache.logging.log4j.spi.ThreadContextStackFactoryTest$VerifiableThreadContextStack")
63+
void testCustomStackRealBehavior() {
64+
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
65+
assertTrue(stack instanceof VerifiableThreadContextStack, "Should be VerifiableThreadContextStack");
66+
67+
VerifiableThreadContextStack verifiableStack = (VerifiableThreadContextStack) stack;
68+
69+
stack.push("operation1");
70+
assertEquals("CUSTOM:operation1", stack.peek(), "Push should add custom prefix");
71+
assertEquals(1, verifiableStack.getCallCount(), "Should track method calls");
72+
73+
stack.push("operation2");
74+
assertEquals("CUSTOM:operation2", stack.peek(), "Second push should also have prefix");
75+
assertEquals(2, verifiableStack.getCallCount(), "Call count should increment");
76+
77+
String popped = stack.pop();
78+
assertEquals("CUSTOM:operation2", popped, "Pop should return prefixed value");
79+
assertEquals(3, verifiableStack.getCallCount(), "Pop should increment call count");
80+
assertEquals("CUSTOM:operation1", stack.peek(), "Remaining item should have prefix");
81+
82+
List<String> stackList = stack.asList();
83+
assertEquals(1, stackList.size(), "Should have one remaining item");
84+
assertEquals("CUSTOM:operation1", stackList.get(0), "List should contain prefixed item");
85+
assertEquals(4, verifiableStack.getCallCount(), "asList should increment call count");
86+
87+
assertTrue(verifiableStack.wasMethodCalled("push"), "Should track push calls");
88+
assertTrue(verifiableStack.wasMethodCalled("pop"), "Should track pop calls");
89+
assertTrue(verifiableStack.wasMethodCalled("asList"), "Should track asList calls");
90+
assertFalse(verifiableStack.wasMethodCalled("clear"), "Should not track uncalled methods");
91+
92+
stack.clear();
93+
assertEquals(5, verifiableStack.getCallCount(), "Clear should also be tracked");
94+
assertTrue(verifiableStack.wasMethodCalled("clear"), "Should track clear call");
95+
}
96+
97+
@Test
98+
@SetTestProperty(key = "log4j2.threadContextStack", value = "com.nonexistent.StackClass")
99+
void testInvalidThreadContextStackClass() {
100+
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
101+
assertNotNull(stack, "ThreadContextStack should not be null");
102+
assertTrue(
103+
stack instanceof DefaultThreadContextStack,
104+
"Should fallback to DefaultThreadContextStack when custom class fails to load");
105+
}
106+
107+
@Test
108+
@SetTestProperty(key = "log4j2.disableThreadContextStack", value = "true")
109+
void testDisabledThreadContextStack() {
110+
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
111+
assertNotNull(stack, "ThreadContextStack should not be null");
112+
assertSame(ThreadContext.NOOP_STACK, stack, "Should return NOOP_STACK when disabled");
113+
}
114+
115+
@Test
116+
@SetTestProperty(key = "log4j2.disableThreadContext", value = "true")
117+
void testDisabledThreadContext() {
118+
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
119+
assertNotNull(stack, "ThreadContextStack should not be null");
120+
assertSame(ThreadContext.NOOP_STACK, stack, "Should return NOOP_STACK when ThreadContext is disabled");
121+
}
122+
123+
@Test
124+
void testFactoryInitDoesNotThrow() {
125+
assertDoesNotThrow(() -> ThreadContextStackFactory.init(), "ThreadContextStackFactory.init() should not throw");
126+
}
127+
128+
@Test
129+
void testFactoryCreateReturnsNonNull() {
130+
final ThreadContextStack stack = ThreadContextStackFactory.createThreadContextStack();
131+
assertNotNull(stack, "createThreadContextStack() should never return null");
132+
}
133+
134+
public static class CustomThreadContextStack extends DefaultThreadContextStack {
135+
public CustomThreadContextStack() {
136+
super();
137+
}
138+
139+
@Override
140+
public String toString() {
141+
return "CustomThreadContextStack";
142+
}
143+
}
144+
145+
public static class VerifiableThreadContextStack extends DefaultThreadContextStack {
146+
147+
private static final String PREFIX = "CUSTOM:";
148+
private int callCount = 0;
149+
private final Set<String> calledMethods = new HashSet<>();
150+
151+
@Override
152+
public void push(String message) {
153+
trackCall("push");
154+
super.push(PREFIX + message);
155+
}
156+
157+
@Override
158+
public String pop() {
159+
trackCall("pop");
160+
return super.pop();
161+
}
162+
163+
@Override
164+
public String peek() {
165+
calledMethods.add("peek");
166+
return super.peek();
167+
}
168+
169+
@Override
170+
public List<String> asList() {
171+
trackCall("asList");
172+
return super.asList();
173+
}
174+
175+
@Override
176+
public void clear() {
177+
trackCall("clear");
178+
super.clear();
179+
}
180+
181+
private void trackCall(String methodName) {
182+
callCount++;
183+
calledMethods.add(methodName);
184+
}
185+
186+
public int getCallCount() {
187+
return callCount;
188+
}
189+
190+
public boolean wasMethodCalled(String methodName) {
191+
return calledMethods.contains(methodName);
192+
}
193+
194+
@Override
195+
public String toString() {
196+
return "VerifiableThreadContextStack[calls=" + callCount + ", methods=" + calledMethods + "]";
197+
}
198+
}
199+
}

log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@
2626
import org.apache.logging.log4j.message.ParameterizedMessage;
2727
import org.apache.logging.log4j.spi.CleanableThreadContextMap;
2828
import org.apache.logging.log4j.spi.DefaultThreadContextMap;
29-
import org.apache.logging.log4j.spi.DefaultThreadContextStack;
3029
import org.apache.logging.log4j.spi.MutableThreadContextStack;
3130
import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
3231
import org.apache.logging.log4j.spi.ThreadContextMap;
3332
import org.apache.logging.log4j.spi.ThreadContextMap2;
3433
import org.apache.logging.log4j.spi.ThreadContextMapFactory;
3534
import org.apache.logging.log4j.spi.ThreadContextStack;
35+
import org.apache.logging.log4j.spi.ThreadContextStackFactory;
3636
import org.apache.logging.log4j.util.PropertiesUtil;
3737
import org.apache.logging.log4j.util.ProviderUtil;
3838

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

199+
public static final ThreadContextStack NOOP_STACK = new NoOpThreadContextStack();
200+
199201
private static final String DISABLE_STACK = "disableThreadContextStack";
200202
private static final String DISABLE_ALL = "disableThreadContext";
201203

@@ -220,8 +222,8 @@ private ThreadContext() {
220222
public static void init() {
221223
final PropertiesUtil properties = PropertiesUtil.getProperties();
222224
contextStack = properties.getBooleanProperty(DISABLE_STACK) || properties.getBooleanProperty(DISABLE_ALL)
223-
? new NoOpThreadContextStack()
224-
: new DefaultThreadContextStack();
225+
? NOOP_STACK
226+
: ThreadContextStackFactory.createThreadContextStack();
225227
// TODO: Fix the tests that need to reset the thread context map to use separate instance of the
226228
// provider instead.
227229
ThreadContextMapFactory.init();

log4j-api/src/main/java/org/apache/logging/log4j/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* @see <a href="https://logging.apache.org/log4j/2.x/manual/api.html">Log4j 2 API manual</a>
3333
*/
3434
@Export
35-
@Version("2.20.2")
35+
@Version("2.21.0")
3636
package org.apache.logging.log4j;
3737

3838
import org.osgi.annotation.bundle.Export;

0 commit comments

Comments
 (0)