diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
index 53512451e35..2cd36d8ab78 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
@@ -27,6 +27,8 @@
/**
* Responsible for initializing the context data of LogEvents. Context data is data that is set by the application to be
* included in all subsequent log events.
+ *
NOTE: It is no longer recommended that custom implementations of this interface be provided as it is
+ * difficult to do. Instead, provide a custom ContextDataProvider.
*
* The source of the context data is implementation-specific. The default source for context data is the ThreadContext.
*
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
index 050d1e0eb07..74925f6cb90 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
@@ -48,6 +48,10 @@ public class ContextDataInjectorFactory {
* {@code ContextDataInjector} classes defined in {@link ThreadContextDataInjector} which is most appropriate for
* the ThreadContext implementation.
*
+ * Note: It is no longer recommended that users provide a custom implementation of the ContextDataInjector.
+ * Instead, provide a {@code ContextDataProvider}.
+ *
+ *
* Users may use this system property to specify the fully qualified class name of a class that implements the
* {@code ContextDataInjector} interface.
*
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
index a24fcb9e35f..f1cd4bc1add 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
@@ -30,7 +30,7 @@
/**
* Provides a read-only {@code StringMap} view of a {@code Map}.
*/
-class JdkMapAdapterStringMap implements StringMap {
+public class JdkMapAdapterStringMap implements StringMap {
private static final long serialVersionUID = -7348247784983193612L;
private static final String FROZEN = "Frozen collection cannot be modified";
private static final Comparator super String> NULL_FIRST_COMPARATOR = new Comparator() {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
index 25f1d031419..0522ecac11a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
@@ -16,14 +16,22 @@
*/
package org.apache.logging.log4j.core.impl;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.ContextDataInjector;
import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.LoaderUtil;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.StringMap;
@@ -44,6 +52,14 @@
*/
public class ThreadContextDataInjector {
+ private static Logger LOGGER = StatusLogger.getLogger();
+
+ /**
+ * ContextDataProviders loaded via OSGi.
+ */
+ public static Collection contextDataProviders =
+ new ConcurrentLinkedDeque<>();
+
/**
* Default {@code ContextDataInjector} for the legacy {@code Map}-based ThreadContext (which is
* also the ThreadContext implementation used for web applications).
@@ -52,24 +68,39 @@ public class ThreadContextDataInjector {
*/
public static class ForDefaultThreadContextMap implements ContextDataInjector {
+ private final List providers;
+
+ public ForDefaultThreadContextMap() {
+ providers = getProviders();
+ }
+
/**
* Puts key-value pairs from both the specified list of properties as well as the thread context into the
* specified reusable StringMap.
*
* @param props list of configuration properties, may be {@code null}
- * @param ignore a {@code StringMap} instance from the log event
+ * @param contextData a {@code StringMap} instance from the log event
* @return a {@code StringMap} combining configuration properties with thread context data
*/
@Override
- public StringMap injectContextData(final List props, final StringMap ignore) {
+ public StringMap injectContextData(final List props, final StringMap contextData) {
- final Map copy = ThreadContext.getImmutableContext();
+ final Map copy;
+
+ if (providers.size() == 1) {
+ copy = providers.get(0).supplyContextData();
+ } else {
+ copy = new HashMap<>();
+ for (ContextDataProvider provider : providers) {
+ copy.putAll(provider.supplyContextData());
+ }
+ }
// The DefaultThreadContextMap stores context data in a Map.
// This is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy.
- // If there are no configuration properties returning a thin wrapper around the copy
+ // If there are no configuration properties or providers returning a thin wrapper around the copy
// is faster than copying the elements into the LogEvent's reusable StringMap.
- if (props == null || props.isEmpty()) {
+ if ((props == null || props.isEmpty())) {
// this will replace the LogEvent's context data with the returned instance.
// NOTE: must mark as frozen or downstream components may attempt to modify (UnsupportedOperationEx)
return copy.isEmpty() ? ContextDataFactory.emptyFrozenContextData() : frozenStringMap(copy);
@@ -114,6 +145,12 @@ public ReadOnlyStringMap rawContextData() {
* This injector always puts key-value pairs into the specified reusable StringMap.
*/
public static class ForGarbageFreeThreadContextMap implements ContextDataInjector {
+ private final List providers;
+
+ public ForGarbageFreeThreadContextMap() {
+ this.providers = getProviders();
+ }
+
/**
* Puts key-value pairs from both the specified list of properties as well as the thread context into the
* specified reusable StringMap.
@@ -128,9 +165,9 @@ public StringMap injectContextData(final List props, final StringMap r
// StringMap. We cannot return the ThreadContext's internal data structure because it may be modified later
// and such modifications should not be reflected in the log event.
copyProperties(props, reusable);
-
- final ReadOnlyStringMap immutableCopy = ThreadContext.getThreadContextMap().getReadOnlyContextData();
- reusable.putAll(immutableCopy);
+ for (int i = 0; i < providers.size(); ++i) {
+ reusable.putAll(providers.get(i).supplyStringMap());
+ }
return reusable;
}
@@ -149,6 +186,11 @@ public ReadOnlyStringMap rawContextData() {
* specified reusable StringMap.
*/
public static class ForCopyOnWriteThreadContextMap implements ContextDataInjector {
+ private final List providers;
+
+ public ForCopyOnWriteThreadContextMap() {
+ this.providers = getProviders();
+ }
/**
* If there are no configuration properties, this injector will return the thread context's internal data
* structure. Otherwise the configuration properties are combined with the thread context key-value pairs into the
@@ -162,17 +204,25 @@ public static class ForCopyOnWriteThreadContextMap implements ContextDataInjecto
public StringMap injectContextData(final List props, final StringMap ignore) {
// If there are no configuration properties we want to just return the ThreadContext's StringMap:
// it is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy.
- final StringMap immutableCopy = ThreadContext.getThreadContextMap().getReadOnlyContextData();
- if (props == null || props.isEmpty()) {
- return immutableCopy; // this will replace the LogEvent's context data with the returned instance
+ if (providers.size() == 1 && (props == null || props.isEmpty())) {
+ // this will replace the LogEvent's context data with the returned instance
+ return providers.get(0).supplyStringMap();
+ }
+ int count = props.size();
+ StringMap[] maps = new StringMap[providers.size()];
+ for (int i = 0; i < providers.size(); ++i) {
+ maps[i] = providers.get(i).supplyStringMap();
+ count += maps[i].size();
}
// However, if the list of Properties is non-empty we need to combine the properties and the ThreadContext
// data. Note that we cannot reuse the specified StringMap: some Loggers may have properties defined
// and others not, so the LogEvent's context data may have been replaced with an immutable copy from
// the ThreadContext - this will throw an UnsupportedOperationException if we try to modify it.
- final StringMap result = ContextDataFactory.createContextData(props.size() + immutableCopy.size());
+ final StringMap result = ContextDataFactory.createContextData(count);
copyProperties(props, result);
- result.putAll(immutableCopy);
+ for (StringMap map : maps) {
+ result.putAll(map);
+ }
return result;
}
@@ -196,4 +246,20 @@ public static void copyProperties(final List properties, final StringM
}
}
}
+
+ private static List getProviders() {
+ final List providers = new ArrayList<>(contextDataProviders);
+ for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
+ try {
+ for (final ContextDataProvider provider : ServiceLoader.load(ContextDataProvider.class, classLoader)) {
+ if (providers.stream().noneMatch((p) -> p.getClass().isAssignableFrom(provider.getClass()))) {
+ providers.add(provider);
+ }
+ }
+ } catch (final Throwable ex) {
+ LOGGER.debug("Unable to access Context Data Providers {}", ex.getMessage());
+ }
+ }
+ return providers;
+ }
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java
new file mode 100644
index 00000000000..230123e3cbd
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java
@@ -0,0 +1,39 @@
+/*
+ * 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.core.impl;
+
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
+import org.apache.logging.log4j.util.StringMap;
+
+import java.util.Map;
+
+/**
+ * ContextDataProvider for ThreadContext data.
+ */
+public class ThreadContextDataProvider implements ContextDataProvider {
+
+ @Override
+ public Map supplyContextData() {
+ return ThreadContext.getImmutableContext();
+ }
+
+ @Override
+ public StringMap supplyStringMap() {
+ return ThreadContext.getThreadContextMap().getReadOnlyContextData();
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
index 4671f6332d1..97aea84fa3f 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
@@ -17,20 +17,26 @@
package org.apache.logging.log4j.core.osgi;
+import java.util.Collection;
import java.util.Hashtable;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.impl.Log4jProvider;
+import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
+import org.apache.logging.log4j.core.impl.ThreadContextDataProvider;
import org.apache.logging.log4j.core.plugins.Log4jPlugins;
import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
import org.apache.logging.log4j.plugins.processor.PluginService;
import org.apache.logging.log4j.spi.Provider;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
/**
@@ -44,6 +50,7 @@ public final class Activator implements BundleActivator {
ServiceRegistration provideRegistration = null;
ServiceRegistration pluginRegistration = null;
+ ServiceRegistration contextDataRegistration = null;
@Override
public void start(final BundleContext context) throws Exception {
@@ -52,7 +59,11 @@ public void start(final BundleContext context) throws Exception {
final Provider provider = new Log4jProvider();
final Hashtable props = new Hashtable<>();
props.put("APIVersion", "2.60");
+ final ContextDataProvider threadContextProvider = new ThreadContextDataProvider();
provideRegistration = context.registerService(Provider.class.getName(), provider, props);
+ contextDataRegistration = context.registerService(ContextDataProvider.class.getName(), threadContextProvider,
+ null);
+ loadContextProviders(context);
// allow the user to override the default ContextSelector (e.g., by using BasicContextSelector for a global cfg)
if (PropertiesUtil.getProperties().getStringProperty(Constants.LOG4J_CONTEXT_SELECTOR) == null) {
System.setProperty(Constants.LOG4J_CONTEXT_SELECTOR, BundleContextSelector.class.getName());
@@ -64,7 +75,21 @@ public void start(final BundleContext context) throws Exception {
public void stop(final BundleContext context) throws Exception {
provideRegistration.unregister();
pluginRegistration.unregister();
+ contextDataRegistration.unregister();
this.contextRef.compareAndSet(context, null);
LogManager.shutdown(false, true);
}
+
+ private static void loadContextProviders(final BundleContext bundleContext) {
+ try {
+ final Collection> serviceReferences =
+ bundleContext.getServiceReferences(ContextDataProvider.class, null);
+ for (final ServiceReference serviceReference : serviceReferences) {
+ final ContextDataProvider provider = bundleContext.getService(serviceReference);
+ ThreadContextDataInjector.contextDataProviders.add(provider);
+ }
+ } catch (final InvalidSyntaxException ex) {
+ LOGGER.error("Error accessing context data provider", ex);
+ }
+ }
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
new file mode 100644
index 00000000000..d302cf8404b
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.core.util;
+
+import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
+import org.apache.logging.log4j.util.StringMap;
+
+import java.util.Map;
+
+/**
+ * Source of context data to be added to each log event.
+ */
+public interface ContextDataProvider {
+
+ /**
+ * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
+ * @return A Map containing the context data or null.
+ */
+ Map supplyContextData();
+
+ /**
+ * Returns the context data as a StringMap.
+ * @return the context data in a StringMap.
+ */
+ default StringMap supplyStringMap() {
+ return new JdkMapAdapterStringMap(supplyContextData());
+ }
+}
diff --git a/log4j-core/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider b/log4j-core/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider
new file mode 100644
index 00000000000..7917658d6db
--- /dev/null
+++ b/log4j-core/src/main/resources/META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider
@@ -0,0 +1 @@
+org.apache.logging.log4j.core.impl.ThreadContextDataProvider
\ No newline at end of file
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java
new file mode 100644
index 00000000000..f52c87dcbdc
--- /dev/null
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.core.util;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ *
+ */
+public class ContextDataProviderTest {
+
+ private static Logger logger;
+ private static ListAppender appender;
+
+ @BeforeClass
+ public static void beforeClass() {
+ ThreadContextDataInjector.contextDataProviders.add(new TestContextDataProvider());
+ System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "target/test-classes/log4j-contextData.xml");
+ LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
+ logger = loggerContext.getLogger(ContextDataProviderTest.class.getName());
+ appender = loggerContext.getConfiguration().getAppender("List");
+ assertNotNull("No List appender", appender);
+ }
+
+ @Test
+ public void testContextProvider() throws Exception {
+ ThreadContext.put("loginId", "jdoe");
+ logger.debug("This is a test");
+ List messages = appender.getMessages();
+ assertEquals("Incorrect number of messages", 1, messages.size());
+ assertTrue("Context data missing", messages.get(0).contains("testKey=testValue"));
+ }
+
+ private static class TestContextDataProvider implements ContextDataProvider {
+
+ @Override
+ public Map supplyContextData() {
+ Map contextData = new HashMap<>();
+ contextData.put("testKey", "testValue");
+ return contextData;
+ }
+
+ }
+}
diff --git a/log4j-core/src/test/resources/log4j-list.xml b/log4j-core/src/test/resources/log4j-contextData.xml
similarity index 87%
rename from log4j-core/src/test/resources/log4j-list.xml
rename to log4j-core/src/test/resources/log4j-contextData.xml
index c2a92c3054f..80e83eefb08 100644
--- a/log4j-core/src/test/resources/log4j-list.xml
+++ b/log4j-core/src/test/resources/log4j-contextData.xml
@@ -15,10 +15,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-
+
-
+
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7c6e6533f08..7bcbf2eea16 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -165,7 +165,12 @@
Update Apache Flume from 1.8.0 to 1.9.0.
-
+
+
+ Add ContextDataProviders as an alternative to having to implement a ContextDataInjector.
+
+
+
Slow initialization on Windows due to accessing network interfaces.
diff --git a/src/site/asciidoc/manual/extending.adoc b/src/site/asciidoc/manual/extending.adoc
index c68668bc4e3..6997928a7ae 100644
--- a/src/site/asciidoc/manual/extending.adoc
+++ b/src/site/asciidoc/manual/extending.adoc
@@ -567,25 +567,17 @@ ListAppender list1 = ListAppender.createAppender("List1", true, false, null, nul
ListAppender list2 = ListAppender.newBuilder().setName("List1").setEntryPerNewLine(true).build();
----
-[#Custom_ContextDataInjector]
-== Custom ContextDataInjector
-
-The `ContextDataInjector` (introduced in Log4j 2.7) is responsible for
-populating the LogEvent's
-link:../log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getContextData()[context
-data] with key-value pairs or replacing it completely. The default
-implementation is `ThreadContextDataInjector`, which obtains context
-attributes from the ThreadContext.
-
-Applications may replace the default `ContextDataInjector` by setting the
-value of the system property `log4j2.contextDataInjector` to the name of
-the custom `ContextDataInjector` class.
-
-Implementors should be aware there are some subtleties related to
-thread-safety and implementing a context data injector in a garbage-free
-manner. See the
-link:../log4j-core/apidocs/org/apache/logging/log4j/core/ContextDataInjector.html[`ContextDataInjector`]
-javadoc for detail.
+[#Custom_ContextDataProvider]
+== Custom ContextDataProvider
+
+
+The `ContextDataProvider` (introduced in Log4j 2.13.2) is an interface applications and libraries can use to inject
+additional key-value pairs into the LogEvent's context data. Log4j's `ThreadContextDataInjector` uses
+`java.util.ServiceLoader` to locate and load `ContextDataProvider` instances. Log4j itself adds the ThreadContext data
+to the LogEvent using `org.apache.logging.log4j.core.impl.ThreadContextDataProvider`. Custom implementations
+should implement the `org.apache.logging.log4j.core.util.ContextDataProvider`interfaceand declare it as a service by
+defining the implmentation class in a file named
+`META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider`.
== Custom ThreadContextMap implementations