You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2024/04/04 10:34:37 UTC
(logging-log4j2) 01/01: Add support for thread context map propagation
This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a commit to branch feature/context-propagation
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 21f95c63a53b0b4d48a6b45d08b7f103e798630b
Author: Piotr P. Karwasz <pi...@karwasz.org>
AuthorDate: Thu Apr 4 12:05:26 2024 +0200
Add support for thread context map propagation
Adds a new `ExtendedThreadContext` SPI class to help external libraries
with an efficient propagation of the `ThreadContextMap`.
Currently integrators can retrieve and set the context map through a
combination of `ThreadContext#getImmutableContext`, `#clear` and
`#putAll`. This inevitable leads to the creation of temporary objects if
the underlying implementation does not use the JDK `Map` class
internally.
As a replacement we provide two `saveMap` and `restoreMap` methods that
can be used to access the `ThreadLocal`s directly. Due to the inherent
unsafety of such operations, these methods are only available from the
`o.a.l.l.spi` package.
We also reimplement `CloseableThreadContext.Instance` to take advantage
of this SPI.
---
log4j-api-test/pom.xml | 11 +-
.../log4j/test/spi/ThreadContextMapSuite.java | 130 +++++++++++++++++++++
.../logging/log4j/test/spi/package-info.java | 26 +++++
.../logging/log4j/CloseableThreadContextTest.java | 5 +-
.../logging/log4j/spi/ThreadContextMapTest.java | 82 ++++++++-----
.../logging/log4j/CloseableThreadContext.java | 42 +++----
.../internal/map/StringArrayThreadContextMap.java | 19 ++-
.../CopyOnWriteSortedArrayThreadContextMap.java | 17 +++
.../logging/log4j/spi/DefaultThreadContextMap.java | 17 +++
.../logging/log4j/spi/ExtendedThreadContext.java | 63 ++++++++++
.../GarbageFreeSortedArrayThreadContextMap.java | 18 +++
.../logging/log4j/spi/NoOpThreadContextMap.java | 10 ++
.../apache/logging/log4j/spi/ThreadContextMap.java | 24 ++++
.../logging/log4j/perf/nogc/OpenHashStringMap.java | 15 +++
.../org/apache/logging/slf4j/MDCContextMap.java | 47 ++++++--
.../apache/logging/slf4j/MDCContextMapTest.java | 52 +++++++--
.../.2.x.x/add_extended_thread_context.xml | 7 ++
17 files changed, 496 insertions(+), 89 deletions(-)
diff --git a/log4j-api-test/pom.xml b/log4j-api-test/pom.xml
index 0ef2578dd8..40d4dc2525 100644
--- a/log4j-api-test/pom.xml
+++ b/log4j-api-test/pom.xml
@@ -37,6 +37,7 @@
<bnd-module-name>org.apache.logging.log4j.test</bnd-module-name>
<bnd-extra-package-options>
org.apache.commons.lang3.*;resolution:=optional,
+ org.assertj.*;resolution:=optional,
<!-- Both JUnit 4 and JUnit 5 are not required -->
org.junit.*;resolution:=optional,
org.hamcrest.*;resolution:=optional,
@@ -48,6 +49,7 @@
<bnd-extra-module-options>
<!-- Non-transitive static modules -->
junit;transitive=false,
+ org.assertj.core;transitive=false,
org.hamcrest;transitive=false,
org.junit.jupiter.api;transitive=false,
org.junitpioneer;transitive=false,
@@ -72,6 +74,10 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
@@ -108,11 +114,6 @@
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
</dependency>
- <dependency>
- <groupId>org.assertj</groupId>
- <artifactId>assertj-core</artifactId>
- <scope>test</scope>
- </dependency>
<!-- Required for JSON support -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ThreadContextMapSuite.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ThreadContextMapSuite.java
new file mode 100644
index 0000000000..9c3bf4aa3f
--- /dev/null
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ThreadContextMapSuite.java
@@ -0,0 +1,130 @@
+/*
+ * 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.test.spi;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import org.apache.logging.log4j.spi.ThreadContextMap;
+
+/**
+ * A simple suite of tests for {@link ThreadContextMap} implementations.
+ */
+public abstract class ThreadContextMapSuite {
+ private static final String KEY = "key";
+
+ /**
+ * Checks if new threads either inherit or do not inherit the context data of the current thread.
+ *
+ * @param threadContext A {@link ThreadContextMap implementation}.
+ * @param key The key to use.
+ * @param expectedValue The expected value on a new thread.
+ */
+ protected static void assertThreadContextValueOnANewThread(
+ final ThreadContextMap threadContext, final String key, final String expectedValue) {
+ final ExecutorService executorService = Executors.newSingleThreadExecutor();
+ try {
+ assertThat(executorService.submit(() -> threadContext.get(key)))
+ .succeedsWithin(Duration.ofSeconds(1))
+ .isEqualTo(expectedValue);
+ } finally {
+ executorService.shutdown();
+ }
+ }
+
+ /**
+ * Ensures that {@code save/restore} works correctly on the current thread.
+ */
+ protected static void assertContextDataCanBeSavedAndRestored(final ThreadContextMap threadContext) {
+ final String externalValue = "externalValue";
+ final String internalValue = "internalValue";
+ final RuntimeException throwable = new RuntimeException();
+ threadContext.put(KEY, externalValue);
+ final Object saved = threadContext.save();
+ try {
+ threadContext.put(KEY, internalValue);
+ assertThat(threadContext.get(KEY)).isEqualTo(internalValue);
+ throw throwable;
+ } catch (final RuntimeException e) {
+ assertThat(e).isSameAs(throwable);
+ assertThat(threadContext.get(KEY)).isEqualTo(internalValue);
+ } finally {
+ threadContext.restore(saved);
+ }
+ assertThat(threadContext.get(KEY)).isEqualTo(externalValue);
+ }
+
+ /**
+ * Ensures that the context data obtained through {@link ThreadContextMap#save} can be safely transferred to another
+ * thread.
+ *
+ * @param threadContext The {@link ThreadContextMap} to test.
+ */
+ protected static void assertContextDataCanBeTransferred(final ThreadContextMap threadContext) {
+ final String mainThreadValue = "mainThreadValue";
+ final String newThreadValue = "newThreadValue";
+ final RuntimeException throwable = new RuntimeException();
+ threadContext.put(KEY, mainThreadValue);
+ final Object mainThreadSaved = threadContext.save();
+ threadContext.remove(KEY);
+ // Move to new thread
+ final ExecutorService executorService = Executors.newSingleThreadExecutor();
+ try {
+ assertThat(executorService.submit(() -> {
+ threadContext.put(KEY, newThreadValue);
+ final Object newThreadSaved = threadContext.restore(mainThreadSaved);
+ try {
+ assertThat(threadContext.get(KEY)).isEqualTo(mainThreadValue);
+ throw throwable;
+ } catch (final RuntimeException e) {
+ assertThat(e).isSameAs(throwable);
+ assertThat(threadContext.get(KEY)).isEqualTo(mainThreadValue);
+ } finally {
+ threadContext.restore(newThreadSaved);
+ }
+ assertThat(threadContext.get(KEY)).isEqualTo(newThreadValue);
+ }))
+ .succeedsWithin(Duration.ofSeconds(1));
+ } finally {
+ executorService.shutdown();
+ }
+ }
+
+ /**
+ * Ensures the {@code restore(null)} can be used to clear the context data.
+ *
+ * @param threadContext The {@link ThreadContextMap} to test.
+ */
+ protected static void assertNullValueClearsTheContextData(final ThreadContextMap threadContext) {
+ final String value = "restoreAcceptsNull";
+ final RuntimeException throwable = new RuntimeException();
+ threadContext.put(KEY, value);
+ final Object saved = threadContext.restore(null);
+ try {
+ assertThat(threadContext.getImmutableMapOrNull()).isNullOrEmpty();
+ throw throwable;
+ } catch (final RuntimeException e) {
+ assertThat(e).isSameAs(throwable);
+ assertThat(threadContext.getImmutableMapOrNull()).isNullOrEmpty();
+ } finally {
+ threadContext.restore(saved);
+ }
+ assertThat(threadContext.get(KEY)).isEqualTo(value);
+ }
+}
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/package-info.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/package-info.java
new file mode 100644
index 0000000000..02cf0ffd78
--- /dev/null
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+/**
+ * Contains helper classes to blackbox test implementations of SPI interfaces.
+ * @since 2.24.0
+ */
+@Export
+@Version("2.24.0")
+package org.apache.logging.log4j.test.spi;
+
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java
index 56e0086a49..153ffa9571 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/CloseableThreadContextTest.java
@@ -23,19 +23,18 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.logging.log4j.test.junit.Log4jStaticResources;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
-import org.junit.jupiter.api.parallel.Resources;
/**
* Tests {@link CloseableThreadContext}.
*
* @since 2.6
*/
-@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)
+@ResourceLock(Log4jStaticResources.THREAD_CONTEXT)
public class CloseableThreadContextTest {
private final String key = "key";
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
index f394e71bea..30f52c4906 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/spi/ThreadContextMapTest.java
@@ -16,61 +16,85 @@
*/
package org.apache.logging.log4j.spi;
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.time.Duration;
import java.util.Properties;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.stream.Stream;
+import org.apache.logging.log4j.internal.map.StringArrayThreadContextMap;
+import org.apache.logging.log4j.test.spi.ThreadContextMapSuite;
+import org.apache.logging.log4j.util.Lazy;
import org.apache.logging.log4j.util.PropertiesUtil;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
-class ThreadContextMapTest {
+@Execution(ExecutionMode.CONCURRENT)
+class ThreadContextMapTest extends ThreadContextMapSuite {
private static final String KEY = "key";
+ private static final Lazy<PropertiesUtil> defaultMapProperties = Lazy.pure(() -> createMapProperties(false));
+ private static final Lazy<PropertiesUtil> inheritableMapProperties = Lazy.pure(() -> createMapProperties(true));
+
+ private static PropertiesUtil createMapProperties(final boolean inheritable) {
+ final Properties props = new Properties();
+ // By specifying all the possible properties, the resulting thread context maps do not depend on other
+ // property sources like Java system properties.
+ props.setProperty("log4j2.threadContextInitialCapacity", "16");
+ props.setProperty("log4j2.isThreadContextMapInheritable", inheritable ? "true" : "false");
+ return new PropertiesUtil(props);
+ }
static Stream<ThreadContextMap> defaultMaps() {
return Stream.of(
new DefaultThreadContextMap(),
new CopyOnWriteSortedArrayThreadContextMap(),
- new GarbageFreeSortedArrayThreadContextMap());
+ new GarbageFreeSortedArrayThreadContextMap(),
+ new StringArrayThreadContextMap());
+ }
+
+ static Stream<ThreadContextMap> localMaps() {
+ return Stream.of(
+ new DefaultThreadContextMap(true, defaultMapProperties.get()),
+ new CopyOnWriteSortedArrayThreadContextMap(defaultMapProperties.get()),
+ new GarbageFreeSortedArrayThreadContextMap(defaultMapProperties.get()),
+ new StringArrayThreadContextMap());
}
static Stream<ThreadContextMap> inheritableMaps() {
- final Properties props = new Properties();
- props.setProperty("log4j2.isThreadContextMapInheritable", "true");
- final PropertiesUtil util = new PropertiesUtil(props);
return Stream.of(
- new DefaultThreadContextMap(true, util),
- new CopyOnWriteSortedArrayThreadContextMap(util),
- new GarbageFreeSortedArrayThreadContextMap(util));
+ new DefaultThreadContextMap(true, inheritableMapProperties.get()),
+ new CopyOnWriteSortedArrayThreadContextMap(inheritableMapProperties.get()),
+ new GarbageFreeSortedArrayThreadContextMap(inheritableMapProperties.get()));
}
@ParameterizedTest
@MethodSource("defaultMaps")
- void threadLocalNotInheritableByDefault(final ThreadContextMap contextMap) {
- contextMap.put(KEY, "threadLocalNotInheritableByDefault");
- verifyThreadContextValueFromANewThread(contextMap, null);
+ void threadLocalNotInheritableByDefault(final ThreadContextMap threadContext) {
+ threadContext.put(KEY, "threadLocalNotInheritableByDefault");
+ assertThreadContextValueOnANewThread(threadContext, KEY, null);
}
@ParameterizedTest
@MethodSource("inheritableMaps")
- void threadLocalInheritableIfConfigured(final ThreadContextMap contextMap) {
- contextMap.put(KEY, "threadLocalInheritableIfConfigured");
- verifyThreadContextValueFromANewThread(contextMap, "threadLocalInheritableIfConfigured");
+ void threadLocalInheritableIfConfigured(final ThreadContextMap threadContext) {
+ threadContext.put(KEY, "threadLocalInheritableIfConfigured");
+ assertThreadContextValueOnANewThread(threadContext, KEY, "threadLocalInheritableIfConfigured");
}
- private static void verifyThreadContextValueFromANewThread(
- final ThreadContextMap contextMap, final String expected) {
- final ExecutorService executorService = Executors.newSingleThreadExecutor();
- try {
- assertThat(executorService.submit(() -> contextMap.get(KEY)))
- .succeedsWithin(Duration.ofSeconds(1))
- .isEqualTo(expected);
- } finally {
- executorService.shutdown();
- }
+ @ParameterizedTest
+ @MethodSource("localMaps")
+ void saveAndRestoreMap(final ThreadContextMap threadContext) {
+ assertContextDataCanBeSavedAndRestored(threadContext);
+ }
+
+ @ParameterizedTest
+ @MethodSource("localMaps")
+ void saveAndRestoreMapOnAnotherThread(final ThreadContextMap threadContext) {
+ assertContextDataCanBeTransferred(threadContext);
+ }
+
+ @ParameterizedTest
+ @MethodSource("localMaps")
+ void restoreAcceptsNull(final ThreadContextMap threadContext) {
+ assertNullValueClearsTheContextData(threadContext);
}
}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java
index c585edf546..d5e23c9bc6 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/CloseableThreadContext.java
@@ -16,10 +16,10 @@
*/
package org.apache.logging.log4j;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.logging.log4j.spi.ExtendedThreadContext;
+import org.apache.logging.log4j.util.Lazy;
/**
* Adds entries to the {@link ThreadContext stack or map} and them removes them when the object is closed, e.g. as part
@@ -102,7 +102,8 @@ public class CloseableThreadContext {
public static class Instance implements AutoCloseable {
private int pushCount = 0;
- private final Map<String, String> originalValues = new HashMap<>();
+ // Instantiated lazily to provide reusability
+ private final Lazy<Object> originalMap = Lazy.lazy(ExtendedThreadContext::saveMap);
private Instance() {}
@@ -114,6 +115,7 @@ public class CloseableThreadContext {
* @return the instance that will back out the changes when closed.
*/
public Instance push(final String message) {
+ originalMap.get();
ThreadContext.push(message);
pushCount++;
return this;
@@ -128,6 +130,7 @@ public class CloseableThreadContext {
* @return the instance that will back out the changes when closed.
*/
public Instance push(final String message, final Object[] args) {
+ originalMap.get();
ThreadContext.push(message, args);
pushCount++;
return this;
@@ -143,10 +146,7 @@ public class CloseableThreadContext {
* @return a new instance that will back out the changes when closed.
*/
public Instance put(final String key, final String value) {
- // If there are no existing values, a null will be stored as an old value
- if (!originalValues.containsKey(key)) {
- originalValues.put(key, ThreadContext.get(key));
- }
+ originalMap.get();
ThreadContext.put(key, value);
return this;
}
@@ -161,13 +161,8 @@ public class CloseableThreadContext {
* @since 2.8
*/
public Instance putAll(final Map<String, String> values) {
- final Map<String, String> currentValues = ThreadContext.getContext();
+ originalMap.get();
ThreadContext.putAll(values);
- for (final String key : values.keySet()) {
- if (!originalValues.containsKey(key)) {
- originalValues.put(key, currentValues.get(key));
- }
- }
return this;
}
@@ -180,6 +175,7 @@ public class CloseableThreadContext {
* @since 2.8
*/
public Instance pushAll(final List<String> messages) {
+ originalMap.get();
for (final String message : messages) {
push(message);
}
@@ -203,22 +199,10 @@ public class CloseableThreadContext {
}
private void closeMap() {
- final Map<String, String> valuesToReplace = new HashMap<>(originalValues.size());
- final List<String> keysToRemove = new ArrayList<>(originalValues.size());
- for (final Map.Entry<String, String> entry : originalValues.entrySet()) {
- final String key = entry.getKey();
- final String originalValue = entry.getValue();
- if (null == originalValue) {
- keysToRemove.add(key);
- } else {
- valuesToReplace.put(key, originalValue);
- }
- }
- if (!valuesToReplace.isEmpty()) {
- ThreadContext.putAll(valuesToReplace);
- }
- if (!keysToRemove.isEmpty()) {
- ThreadContext.removeAll(keysToRemove);
+ if (originalMap.isInitialized()) {
+ ExtendedThreadContext.restoreMap(originalMap.get());
+ // Prepare to be reused
+ originalMap.set(null);
}
}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
index 608ca77ffb..b5c79caab4 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/map/StringArrayThreadContextMap.java
@@ -43,7 +43,7 @@ public class StringArrayThreadContextMap implements ThreadContextMap, ReadOnlySt
*/
public static final String INHERITABLE_MAP = "isThreadContextMapInheritable";
- private ThreadLocal<Object[]> threadLocalMapState;
+ private final ThreadLocal<Object[]> threadLocalMapState;
public StringArrayThreadContextMap() {
threadLocalMapState = new ThreadLocal<>();
@@ -160,6 +160,23 @@ public class StringArrayThreadContextMap implements ThreadContextMap, ReadOnlySt
return UnmodifiableArrayBackedMap.getInstance(state).size();
}
+ @Override
+ public Object save() {
+ return threadLocalMapState.get();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object restore(final Object contextMap) {
+ final Object current = save();
+ if (contextMap == null) {
+ clear();
+ } else {
+ threadLocalMapState.set((Object[]) contextMap);
+ }
+ return current;
+ }
+
@Override
public String toString() {
final Object[] state = threadLocalMapState.get();
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
index ca9f501c21..98b2a35cca 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/CopyOnWriteSortedArrayThreadContextMap.java
@@ -221,6 +221,23 @@ class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
return map == null || map.isEmpty();
}
+ @Override
+ public Object save() {
+ return localMap.get();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object restore(final Object contextMap) {
+ final Object current = save();
+ if (contextMap == null) {
+ clear();
+ } else {
+ localMap.set((StringMap) contextMap);
+ }
+ return current;
+ }
+
@Override
public String toString() {
final StringMap map = localMap.get();
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
index a07992d371..16b0d4266e 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java
@@ -231,4 +231,21 @@ public class DefaultThreadContextMap implements ThreadContextMap, ReadOnlyString
final Map<String, String> otherMap = other.getImmutableMapOrNull();
return Objects.equals(map, otherMap);
}
+
+ @Override
+ public Object save() {
+ return localMap.get();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object restore(final Object contextMap) {
+ final Object current = save();
+ if (contextMap == null) {
+ clear();
+ } else {
+ localMap.set((Map<String, String>) contextMap);
+ }
+ return current;
+ }
}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ExtendedThreadContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ExtendedThreadContext.java
new file mode 100644
index 0000000000..6af0c06b49
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ExtendedThreadContext.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+/**
+ * Provides advanced access method to the {@link ThreadContextMap}.
+ * @since 2.24.0
+ */
+public final class ExtendedThreadContext {
+
+ private ExtendedThreadContext() {}
+
+ /**
+ * Saves the context data of the current thread
+ * <p>
+ * Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
+ * underlying context data will not be reflected in the returned object.
+ * </p>
+ * @return An opaque representation of the context data.
+ * @see #restoreMap
+ */
+ public static Object saveMap() {
+ return getThreadContextMap().save();
+ }
+
+ /**
+ * Restores the context data of the current thread from a saved version
+ * @param contextMap An opaque representation of the context data obtained through a previous call to
+ * {@link #saveMap} or {@code restoreMap}.
+ * @return An opaque representation of the current thread's context data.
+ * @see #saveMap
+ */
+ public static Object restoreMap(final Object contextMap) {
+ return getThreadContextMap().restore(contextMap);
+ }
+
+ /**
+ * Clears the context data of the current thread
+ */
+ public static void clearMap() {
+ getThreadContextMap().clear();
+ }
+
+ private static ThreadContextMap getThreadContextMap() {
+ return ProviderUtil.getProvider().getThreadContextMapInstance();
+ }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
index 1622a0c455..07c1dc10e6 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/GarbageFreeSortedArrayThreadContextMap.java
@@ -210,6 +210,24 @@ class GarbageFreeSortedArrayThreadContextMap implements ReadOnlyThreadContextMap
return map == null || map.isEmpty();
}
+ @Override
+ public Object save() {
+ final StringMap map = localMap.get();
+ return map != null && !map.isEmpty() ? createStringMap(map) : null;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Object restore(final Object contextMap) {
+ final Object current = save();
+ if (contextMap == null) {
+ clear();
+ } else {
+ localMap.set((StringMap) contextMap);
+ }
+ return current;
+ }
+
@Override
public String toString() {
final StringMap map = localMap.get();
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
index e79626f25a..96964720d2 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/NoOpThreadContextMap.java
@@ -62,4 +62,14 @@ public class NoOpThreadContextMap implements ThreadContextMap {
@Override
public void remove(final String key) {}
+
+ @Override
+ public Object save() {
+ return null;
+ }
+
+ @Override
+ public Object restore(final Object contextMap) {
+ return null;
+ }
}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
index f0af90ddbb..2d0eba1ea8 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextMap.java
@@ -18,6 +18,7 @@ package org.apache.logging.log4j.spi;
import java.util.Map;
import org.apache.logging.log4j.ThreadContext;
+import org.osgi.annotation.versioning.ProviderType;
/**
* Service provider interface to implement custom MDC behavior for {@link org.apache.logging.log4j.ThreadContext}.
@@ -26,6 +27,7 @@ import org.apache.logging.log4j.ThreadContext;
* are accessible to applications via the {@link ThreadContext#getThreadContextMap()} method.
* </p>
*/
+@ProviderType
public interface ThreadContextMap {
/**
@@ -33,6 +35,28 @@ public interface ThreadContextMap {
*/
void clear();
+ /**
+ * Saves the context data of the current thread
+ * <p>
+ * Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
+ * underlying context data will not be reflected in the returned object.
+ * </p>
+ * @return An opaque representation of the context data.
+ * @since 2.24.0
+ * @see #restore
+ */
+ Object save();
+
+ /**
+ * Restores the context data of the current thread from a saved version
+ * @param contextMap An opaque representation of the context data obtained through a previous call to
+ * {@link #save} or {@code restore}.
+ * @return An opaque representation of the current context data.
+ * @since 2.24.0
+ * @see #save
+ */
+ Object restore(Object contextMap);
+
/**
* Determines if the key is in the context.
* @param key The key to locate.
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java
index 2e26a3b152..0c69e7eef8 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/nogc/OpenHashStringMap.java
@@ -798,6 +798,21 @@ public class OpenHashStringMap<K, V> implements StringMap, ThreadContextMap {
return sb.toString();
}
+ @Override
+ public Object save() {
+ return getImmutableMapOrNull();
+ }
+
+ @Override
+ public Object restore(final Object contextMap) {
+ final Object current = save();
+ clear();
+ if (contextMap instanceof Map) {
+ putAll((Map<? extends K, ? extends V>) contextMap);
+ }
+ return current;
+ }
+
private static final class HashCommon {
private HashCommon() {}
diff --git a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java
index 4f2ef190bb..b136ead6df 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/MDCContextMap.java
@@ -23,6 +23,7 @@ import org.apache.logging.log4j.spi.CleanableThreadContextMap;
import org.apache.logging.log4j.util.SortedArrayStringMap;
import org.apache.logging.log4j.util.StringMap;
import org.slf4j.MDC;
+import org.slf4j.spi.MDCAdapter;
/**
* Bind the ThreadContextMap to the SLF4J MDC.
@@ -35,60 +36,86 @@ public class MDCContextMap implements CleanableThreadContextMap {
EMPTY_CONTEXT_DATA.freeze();
}
+ private final MDCAdapter mdc;
+
+ public MDCContextMap() {
+ this(MDC.getMDCAdapter());
+ }
+
+ MDCContextMap(final MDCAdapter mdc) {
+ this.mdc = mdc;
+ }
+
@Override
public void put(final String key, final String value) {
- MDC.put(key, value);
+ mdc.put(key, value);
}
@Override
public void putAll(final Map<String, String> m) {
for (final Entry<String, String> entry : m.entrySet()) {
- MDC.put(entry.getKey(), entry.getValue());
+ mdc.put(entry.getKey(), entry.getValue());
}
}
@Override
public String get(final String key) {
- return MDC.get(key);
+ return mdc.get(key);
}
@Override
public void remove(final String key) {
- MDC.remove(key);
+ mdc.remove(key);
}
@Override
public void removeAll(final Iterable<String> keys) {
for (final String key : keys) {
- MDC.remove(key);
+ mdc.remove(key);
}
}
@Override
public void clear() {
- MDC.clear();
+ mdc.clear();
+ }
+
+ @Override
+ public Object save() {
+ return mdc.getCopyOfContextMap();
+ }
+
+ @Override
+ public Object restore(final Object contextMap) {
+ final Object current = save();
+ if (contextMap == null) {
+ mdc.clear();
+ } else {
+ mdc.setContextMap((Map<String, String>) contextMap);
+ }
+ return current;
}
@Override
public boolean containsKey(final String key) {
- final Map<String, String> map = MDC.getCopyOfContextMap();
+ final Map<String, String> map = mdc.getCopyOfContextMap();
return map != null && map.containsKey(key);
}
@Override
public Map<String, String> getCopy() {
- final Map<String, String> contextMap = MDC.getCopyOfContextMap();
+ final Map<String, String> contextMap = mdc.getCopyOfContextMap();
return contextMap != null ? contextMap : new HashMap<>();
}
@Override
public Map<String, String> getImmutableMapOrNull() {
- return MDC.getCopyOfContextMap();
+ return mdc.getCopyOfContextMap();
}
@Override
public boolean isEmpty() {
- final Map<String, String> map = MDC.getCopyOfContextMap();
+ final Map<String, String> map = mdc.getCopyOfContextMap();
return map == null || map.isEmpty();
}
diff --git a/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/MDCContextMapTest.java b/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/MDCContextMapTest.java
index 6673de4597..b35be1013e 100644
--- a/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/MDCContextMapTest.java
+++ b/log4j-to-slf4j/src/test/java/org/apache/logging/slf4j/MDCContextMapTest.java
@@ -24,27 +24,55 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.test.spi.ThreadContextMapSuite;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junitpioneer.jupiter.Issue;
-import org.slf4j.MDCTestHelper;
+import org.slf4j.helpers.BasicMDCAdapter;
import org.slf4j.spi.MDCAdapter;
-class MDCContextMapTest {
+@Execution(ExecutionMode.CONCURRENT)
+class MDCContextMapTest extends ThreadContextMapSuite {
+
+ private static final String KEY = "key";
@Test
@Issue("https://github.com/apache/logging-log4j2/issues/1426")
void nonNullGetCopy() {
- final ThreadContextMap contextMap = new MDCContextMap();
final MDCAdapter mockAdapter = mock(MDCAdapter.class);
+ final ThreadContextMap contextMap = new MDCContextMap(mockAdapter);
when(mockAdapter.getCopyOfContextMap()).thenReturn(null);
- final MDCAdapter adapter = MDCTestHelper.replaceMDCAdapter(mockAdapter);
- try {
- assertThat(contextMap.getImmutableMapOrNull()).isNull();
- assertThat(contextMap.getCopy()).isNotNull();
- verify(mockAdapter, times(2)).getCopyOfContextMap();
- verifyNoMoreInteractions(mockAdapter);
- } finally {
- MDCTestHelper.replaceMDCAdapter(adapter);
- }
+
+ assertThat(contextMap.getImmutableMapOrNull()).isNullOrEmpty();
+ assertThat(contextMap.getCopy()).isNotNull();
+ verify(mockAdapter, times(2)).getCopyOfContextMap();
+ verifyNoMoreInteractions(mockAdapter);
+ }
+
+ private ThreadContextMap createThreadContextMap() {
+ return new MDCContextMap(new BasicMDCAdapter());
+ }
+
+ @Test
+ void threadLocalInheritable() {
+ final ThreadContextMap threadContext = createThreadContextMap();
+ threadContext.put(KEY, "threadLocalInheritable");
+ assertThreadContextValueOnANewThread(threadContext, KEY, "threadLocalInheritable");
+ }
+
+ @Test
+ void saveAndRestoreMap() {
+ assertContextDataCanBeSavedAndRestored(createThreadContextMap());
+ }
+
+ @Test
+ void saveAndRestoreMapOnAnotherThread() {
+ assertContextDataCanBeTransferred(createThreadContextMap());
+ }
+
+ @Test
+ void restoreAcceptsNull() {
+ assertNullValueClearsTheContextData(createThreadContextMap());
}
}
diff --git a/src/changelog/.2.x.x/add_extended_thread_context.xml b/src/changelog/.2.x.x/add_extended_thread_context.xml
new file mode 100644
index 0000000000..e2a1ab575e
--- /dev/null
+++ b/src/changelog/.2.x.x/add_extended_thread_context.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://logging.apache.org/log4j/changelog"
+ xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.3.xsd"
+ type="added">
+ <description format="asciidoc">Add `ExtendedThreadContext` SPI class to help the propagation of the thread context map.</description>
+</entry>