You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2020/03/03 06:49:58 UTC

[logging-log4j2] branch master updated: LOG4J2-2779 - Add ContextDataProviders as an alternative to having to implement a ContextDataInjector.

This is an automated email from the ASF dual-hosted git repository.

rgoers pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/master by this push:
     new c4c2808  LOG4J2-2779 - Add ContextDataProviders as an alternative to having to implement a ContextDataInjector.
c4c2808 is described below

commit c4c28085ad179578b396dd1def12c22209583cbf
Author: Ralph Goers <rg...@apache.org>
AuthorDate: Mon Mar 2 23:49:46 2020 -0700

    LOG4J2-2779 - Add ContextDataProviders as an alternative to having to implement a ContextDataInjector.
---
 .../logging/log4j/core/ContextDataInjector.java    |  2 +
 .../core/impl/ContextDataInjectorFactory.java      |  4 +
 .../log4j/core/impl/JdkMapAdapterStringMap.java    |  2 +-
 .../log4j/core/impl/ThreadContextDataInjector.java | 92 +++++++++++++++++++---
 .../log4j/core/impl/ThreadContextDataProvider.java | 39 +++++++++
 .../apache/logging/log4j/core/osgi/Activator.java  | 25 ++++++
 .../log4j/core/util/ContextDataProvider.java       | 42 ++++++++++
 ...che.logging.log4j.core.util.ContextDataProvider |  1 +
 .../log4j/core/util/ContextDataProviderTest.java   | 74 +++++++++++++++++
 .../{log4j-list.xml => log4j-contextData.xml}      |  4 +-
 src/changes/changes.xml                            |  7 +-
 src/site/asciidoc/manual/extending.adoc            | 30 +++----
 12 files changed, 286 insertions(+), 36 deletions(-)

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 5351245..2cd36d8 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 @@ import org.apache.logging.log4j.util.StringMap;
 /**
  * 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.
+ * <p><b>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.</b></p>
  * <p>
  * The source of the context data is implementation-specific. The default source for context data is the ThreadContext.
  * </p><p>
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 050d1e0..74925f6 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.
      * <p>
+     * <b>Note:</b> It is no longer recommended that users provide a custom implementation of the ContextDataInjector.
+     * Instead, provide a {@code ContextDataProvider}.
+     * </p>
+     * <p>
      * Users may use this system property to specify the fully qualified class name of a class that implements the
      * {@code ContextDataInjector} interface.
      * </p><p>
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 a24fcb9..f1cd4bc 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 @@ import org.apache.logging.log4j.util.TriConsumer;
 /**
  * Provides a read-only {@code StringMap} view of a {@code Map<String, String>}.
  */
-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<String>() {
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 25f1d03..0522eca 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 @@ import org.apache.logging.log4j.util.StringMap;
  */
 public class ThreadContextDataInjector {
 
+    private static Logger LOGGER = StatusLogger.getLogger();
+
+    /**
+     * ContextDataProviders loaded via OSGi.
+     */
+    public static Collection<ContextDataProvider> contextDataProviders =
+            new ConcurrentLinkedDeque<>();
+
     /**
      * Default {@code ContextDataInjector} for the legacy {@code Map<String, String>}-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<ContextDataProvider> 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<Property> props, final StringMap ignore) {
+        public StringMap injectContextData(final List<Property> props, final StringMap contextData) {
 
-            final Map<String, String> copy = ThreadContext.getImmutableContext();
+            final Map<String, String> 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<String, String>.
             // 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 class ThreadContextDataInjector {
      * This injector always puts key-value pairs into the specified reusable StringMap.
      */
     public static class ForGarbageFreeThreadContextMap implements ContextDataInjector {
+        private final List<ContextDataProvider> 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 class ThreadContextDataInjector {
             // 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 class ThreadContextDataInjector {
      * specified reusable StringMap.
      */
     public static class ForCopyOnWriteThreadContextMap implements ContextDataInjector {
+        private final List<ContextDataProvider> 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 class ThreadContextDataInjector {
         public StringMap injectContextData(final List<Property> 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 class ThreadContextDataInjector {
             }
         }
     }
+
+    private static List<ContextDataProvider> getProviders() {
+        final List<ContextDataProvider> 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 0000000..230123e
--- /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<String, String> 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 4671f63..97aea84 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 final class Activator implements BundleActivator {
         final Provider provider = new Log4jProvider();
         final Hashtable<String, String> 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 final class Activator implements BundleActivator {
     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<ServiceReference<ContextDataProvider>> serviceReferences =
+                    bundleContext.getServiceReferences(ContextDataProvider.class, null);
+            for (final ServiceReference<ContextDataProvider> 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 0000000..d302cf8
--- /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<String, String> 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 0000000..7917658
--- /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 0000000..f52c87d
--- /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<String> 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<String, String> supplyContextData() {
+            Map<String, String> 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 c2a92c3..80e83ee 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.
   -->
-<Configuration status="WARN">
+<Configuration status="WARN" name="Context Data Test">
   <Appenders>
     <List name="List">
-      <PatternLayout pattern="[%-5level] %c{1.} %msg%n" />
+      <PatternLayout pattern="[%-5level] ThreadContext%X %c{1.} %msg%n" />
     </List>
   </Appenders>
   <Loggers>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7c6e653..7bcbf2e 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.
       </action>
     </release>
-    <release version="2.13.1" date="2019-MM-DD" description="GA Release 2.13.1">
+    <release version="2.13.2" date="2020-MM-DD" description="GA Release 2.13.2">
+      <action issue="LOG4J2-2779" dev="rgoers" type="update">
+        Add ContextDataProviders as an alternative to having to implement a ContextDataInjector.
+      </action>
+    </release>
+    <release version="2.13.1" date="2020-02-25" description="GA Release 2.13.1">
       <action issue="LOG4J2-2717" dev="rgoers" type="fix">
         Slow initialization on Windows due to accessing network interfaces.
       </action>
diff --git a/src/site/asciidoc/manual/extending.adoc b/src/site/asciidoc/manual/extending.adoc
index c68668b..6997928 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