From cd6cdd342c68d49bdd1af85c1b721594eaf2f957 Mon Sep 17 00:00:00 2001 From: yybmion Date: Wed, 9 Jul 2025 16:56:18 +0900 Subject: [PATCH] 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 --- .../logging/log4j/ThreadContextTest.java | 52 +++++ .../spi/ThreadContextStackFactoryTest.java | 199 ++++++++++++++++++ .../apache/logging/log4j/ThreadContext.java | 8 +- .../apache/logging/log4j/package-info.java | 2 +- .../apache/logging/log4j/spi/Provider.java | 83 +++++++- .../log4j/spi/ThreadContextStackFactory.java | 50 +++++ .../logging/log4j/spi/package-info.java | 2 +- .../log4j/core/impl/Log4jProvider.java | 30 +++ .../logging/log4j/core/impl/package-info.java | 2 +- .../logging/log4j/core/package-info.java | 2 +- .../1507_add_threadContextStack_injection.xml | 12 ++ 11 files changed, 433 insertions(+), 9 deletions(-) create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextStackFactoryTest.java create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextStackFactory.java create mode 100644 src/changelog/.2.x.x/1507_add_threadContextStack_injection.xml diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextTest.java index a06fd7c2611..32ac4ee2e59 100644 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextTest.java +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/ThreadContextTest.java @@ -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; diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextStackFactoryTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextStackFactoryTest.java new file mode 100644 index 00000000000..5eeb18ddcef --- /dev/null +++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextStackFactoryTest.java @@ -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 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 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 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 + "]"; + } + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java index 46dba0dd7d8..15e4732c99d 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/ThreadContext.java @@ -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; @@ -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"; @@ -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(); diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java b/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java index 5b260752271..708eef3ba9c 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java @@ -32,7 +32,7 @@ * @see Log4j 2 API manual */ @Export -@Version("2.20.2") +@Version("2.21.0") package org.apache.logging.log4j; import org.osgi.annotation.bundle.Export; diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java index 4b9ab3f8d51..f0ec2630da4 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java @@ -21,6 +21,7 @@ import java.util.Objects; import java.util.Properties; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.simple.SimpleLoggerContextFactory; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.LoaderUtil; @@ -79,6 +80,7 @@ public class Provider { private static final String DISABLE_CONTEXT_MAP = "log4j2.disableThreadContextMap"; private static final String DISABLE_THREAD_CONTEXT = "log4j2.disableThreadContext"; + private static final String DISABLE_CONTEXT_STACK = "log4j2.disableThreadContextStack"; private static final int DEFAULT_PRIORITY = -1; private static final Logger LOGGER = StatusLogger.getLogger(); @@ -94,6 +96,7 @@ public class Provider { private final @Nullable String threadContextMap; private final @Nullable Class threadContextMapClass; + private final @Nullable Class threadContextStackClass; private final @Nullable String versions; @Deprecated @@ -117,6 +120,7 @@ public Provider(final Properties props, final URL url, final ClassLoader classLo threadContextMap = props.getProperty(THREAD_CONTEXT_MAP); loggerContextFactoryClass = null; threadContextMapClass = null; + threadContextStackClass = null; versions = null; } @@ -126,7 +130,7 @@ public Provider(final Properties props, final URL url, final ClassLoader classLo * @since 2.24.0 */ public Provider(final @Nullable Integer priority, final String versions) { - this(priority, versions, null, null); + this(priority, versions, null, null, null); } /** @@ -140,7 +144,7 @@ public Provider( final @Nullable Integer priority, final String versions, final @Nullable Class loggerContextFactoryClass) { - this(priority, versions, loggerContextFactoryClass, null); + this(priority, versions, loggerContextFactoryClass, null, null); } /** @@ -157,10 +161,31 @@ public Provider( final String versions, final @Nullable Class loggerContextFactoryClass, final @Nullable Class threadContextMapClass) { + this(priority, versions, loggerContextFactoryClass, threadContextMapClass, null); + } + + /** + * @param priority A positive number specifying the provider's priority or {@code null} if default, + * @param versions Minimal API version required, should be set to {@link #CURRENT_VERSION}, + * @param loggerContextFactoryClass A public exported implementation of {@link LoggerContextFactory} or {@code + * null} if {@link #getLoggerContextFactory()} is also implemented, + * @param threadContextMapClass A public exported implementation of {@link ThreadContextMap} or {@code null} if + * {@link #getThreadContextMapInstance()} is implemented, + * @param threadContextStackClass A public exported implementation of {@link ThreadContextStack} or {@code null} if + * {@link #getThreadContextStackInstance()} is implemented. + * @since 2.26.0 + */ + public Provider( + final @Nullable Integer priority, + final String versions, + final @Nullable Class loggerContextFactoryClass, + final @Nullable Class threadContextMapClass, + final @Nullable Class threadContextStackClass) { this.priority = priority != null ? priority : DEFAULT_PRIORITY; this.versions = versions; this.loggerContextFactoryClass = loggerContextFactoryClass; this.threadContextMapClass = threadContextMapClass; + this.threadContextStackClass = threadContextStackClass; // Deprecated className = null; threadContextMap = null; @@ -313,6 +338,56 @@ public ThreadContextMap getThreadContextMapInstance() { : new DefaultThreadContextMap(); } + /** + * Gets the class name of the {@link ThreadContextStack} implementation of this Provider. + * + * @return the class name of a ThreadContextStack implementation + */ + public @Nullable String getThreadContextStack() { + return threadContextStackClass != null ? threadContextStackClass.getName() : null; + } + + /** + * Loads the {@link ThreadContextStack} class specified by this Provider. + * + * @return the {@code ThreadContextStack} implementation class or {@code null} if unspecified or a loading error + * occurred. + */ + public @Nullable Class loadThreadContextStack() { + return threadContextStackClass; + } + + /** + * @return The thread context stack to be used by {@link org.apache.logging.log4j.ThreadContext}. + */ + public ThreadContextStack getThreadContextStackInstance() { + final Class implementation = loadThreadContextStack(); + if (implementation != null) { + try { + return LoaderUtil.newInstanceOf(implementation); + } catch (final ReflectiveOperationException e) { + LOGGER.error("Failed to instantiate thread context stack {}.", implementation.getName(), e); + } + } + + final PropertiesUtil props = PropertiesUtil.getProperties(); + + if (props.getBooleanProperty(DISABLE_CONTEXT_STACK) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) { + return ThreadContext.NOOP_STACK; + } + + String threadContextStackClass = props.getStringProperty("log4j2.threadContextStack"); + if (threadContextStackClass != null) { + try { + return LoaderUtil.newCheckedInstanceOf(threadContextStackClass, ThreadContextStack.class); + } catch (final Exception e) { + LOGGER.error("Unable to create instance of class {}.", threadContextStackClass, e); + } + } + + return new DefaultThreadContextStack(); + } + /** * Gets the URL containing this Provider's Log4j details. * @@ -333,6 +408,10 @@ public String toString() { result.append("\n\tpriority = ").append(priority); } final String threadContextMap = getThreadContextMap(); + final String threadContextStack = getThreadContextStack(); + if (threadContextStack != null) { + result.append("\n\tthreadContextStack = ").append(threadContextStack); + } if (threadContextMap != null) { result.append("\n\tthreadContextMap = ").append(threadContextMap); } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextStackFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextStackFactory.java new file mode 100644 index 00000000000..e6bf8f58aa2 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextStackFactory.java @@ -0,0 +1,50 @@ +/* + * 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 org.apache.logging.log4j.util.ProviderUtil; + +/** + * Creates the ThreadContextStack instance used by the ThreadContext. + *

+ * Any custom {@code ThreadContextStack} can be installed by setting system property + * {@code log4j2.threadContextStack} to the fully qualified class name of the class implementing the + * {@code ThreadContextStack} interface. + *

+ * Instead of system properties, the above can also be specified in a properties file named + * {@code log4j2.component.properties} in the classpath. + *

+ * + * @see ThreadContextStack + * @see org.apache.logging.log4j.ThreadContext + */ +public final class ThreadContextStackFactory { + + /** + * Initializes static variables based on system properties. Normally called when this class is initialized by the VM + * and when Log4j is reconfigured. + */ + public static void init() { + ProviderUtil.getProvider().getThreadContextStackInstance(); + } + + private ThreadContextStackFactory() {} + + public static ThreadContextStack createThreadContextStack() { + return ProviderUtil.getProvider().getThreadContextStackInstance(); + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java index 3b6b5c25857..0956aad0030 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java @@ -19,7 +19,7 @@ * API classes. */ @Export -@Version("2.25.0") +@Version("2.26.0") package org.apache.logging.log4j.spi; import org.osgi.annotation.bundle.Export; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java index ffcdb2e643a..10786530915 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java @@ -19,12 +19,15 @@ import aQute.bnd.annotation.Resolution; import aQute.bnd.annotation.spi.ServiceProvider; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.core.context.internal.GarbageFreeSortedArrayThreadContextMap; import org.apache.logging.log4j.spi.DefaultThreadContextMap; +import org.apache.logging.log4j.spi.DefaultThreadContextStack; import org.apache.logging.log4j.spi.LoggerContextFactory; import org.apache.logging.log4j.spi.NoOpThreadContextMap; import org.apache.logging.log4j.spi.Provider; import org.apache.logging.log4j.spi.ThreadContextMap; +import org.apache.logging.log4j.spi.ThreadContextStack; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.Lazy; import org.apache.logging.log4j.util.LoaderUtil; @@ -77,6 +80,7 @@ public class Log4jProvider extends Provider { private static final String DISABLE_THREAD_CONTEXT = "log4j2.disableThreadContext"; private static final String THREAD_CONTEXT_MAP_PROPERTY = "log4j2.threadContextMap"; private static final String GC_FREE_THREAD_CONTEXT_PROPERTY = "log4j2.garbagefree.threadContextMap"; + private static final String THREAD_CONTEXT_STACK_PROPERTY = "log4j2.threadContextStack"; // Name of the context map implementations private static final String WEB_APP_CLASS_NAME = "org.apache.logging.log4j.spi.DefaultThreadContextMap"; @@ -87,6 +91,7 @@ public class Log4jProvider extends Provider { private final Lazy loggerContextFactoryLazy = Lazy.lazy(Log4jContextFactory::new); private final Lazy threadContextMapLazy = Lazy.lazy(this::createThreadContextMap); + private final Lazy threadContextStackLazy = Lazy.lazy(this::createThreadContextStack); public Log4jProvider() { super(10, CURRENT_VERSION, Log4jContextFactory.class); @@ -102,6 +107,11 @@ public ThreadContextMap getThreadContextMapInstance() { return threadContextMapLazy.get(); } + @Override + public ThreadContextStack getThreadContextStackInstance() { + return threadContextStackLazy.get(); + } + private ThreadContextMap createThreadContextMap() { // Properties final PropertiesUtil props = PropertiesUtil.getProperties(); @@ -143,8 +153,28 @@ private ThreadContextMap createThreadContextMap() { return NoOpThreadContextMap.INSTANCE; } + private ThreadContextStack createThreadContextStack() { + final PropertiesUtil props = PropertiesUtil.getProperties(); + if (props.getBooleanProperty(DISABLE_CONTEXT_MAP) || props.getBooleanProperty(DISABLE_THREAD_CONTEXT)) { + return ThreadContext.NOOP_STACK; + } + String threadContextStackClass = props.getStringProperty(THREAD_CONTEXT_STACK_PROPERTY); + if (threadContextStackClass != null) { + try { + return LoaderUtil.newCheckedInstanceOf(threadContextStackClass, ThreadContextStack.class); + } catch (final Exception e) { + LOGGER.error("Unable to create instance of class {}.", threadContextStackClass, e); + } + } + return new DefaultThreadContextStack(); + } + // Used in tests void resetThreadContextMap() { threadContextMapLazy.set(null); } + + void resetThreadContextStack() { + threadContextStackLazy.set(null); + } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java index 666e8325ed8..2d4bdcd199c 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java @@ -18,7 +18,7 @@ * Log4j 2 private implementation classes. */ @Export -@Version("2.24.1") +@Version("2.25.0") package org.apache.logging.log4j.core.impl; import org.osgi.annotation.bundle.Export; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java index 00552976b76..171f9c7e78d 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java @@ -18,7 +18,7 @@ * Implementation of Log4j 2. */ @Export -@Version("2.24.2") +@Version("2.25.0") package org.apache.logging.log4j.core; import org.osgi.annotation.bundle.Export; diff --git a/src/changelog/.2.x.x/1507_add_threadContextStack_injection.xml b/src/changelog/.2.x.x/1507_add_threadContextStack_injection.xml new file mode 100644 index 00000000000..2883978fed2 --- /dev/null +++ b/src/changelog/.2.x.x/1507_add_threadContextStack_injection.xml @@ -0,0 +1,12 @@ + + + + + Add ThreadContextStackFactory following ThreadContextMapFactory pattern + +