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 2024/04/22 04:27:26 UTC

(logging-log4j2) branch main updated: Add scoped context support. Remove ThreadContextDataInjector. (#2494)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new f95b50babf Add scoped context support. Remove ThreadContextDataInjector. (#2494)
f95b50babf is described below

commit f95b50babfd4c768037a74b5f3fd483806e7c735
Author: Ralph Goers <rg...@apache.org>
AuthorDate: Sun Apr 21 21:27:20 2024 -0700

    Add scoped context support. Remove ThreadContextDataInjector. (#2494)
    
    * Add ScopedContext. Remove ThreadContextDataInjector
    
    * Import references in javadoc
---
 .../logging/log4j/async/logger/AsyncLogger.java    |  20 +-
 .../log4j/async/logger/RingBufferLogEvent.java     |   4 +-
 .../async/logger/RingBufferLogEventTranslator.java |  14 +-
 .../logging/log4j/core/LogEventFactoryTest.java    |  19 +-
 .../core/impl/ThreadContextDataInjectorTest.java   | 178 --------------
 .../log4j/core/util/ContextDataProviderTest.java   |  23 +-
 .../logging/log4j/core/ContextDataInjector.java    | 112 ---------
 .../org/apache/logging/log4j/core/LogEvent.java    |   4 +-
 .../log4j/core/filter/DynamicThresholdFilter.java  |  33 +--
 .../core/filter/MutableThreadContextMapFilter.java |   3 -
 .../log4j/core/filter/ThreadContextMapFilter.java  |  28 +--
 .../logging/log4j/core/impl/ContextData.java       | 107 ++++++++
 .../log4j/core/impl/ContextDataFactory.java        |   5 +-
 .../core/impl/ContextDataInjectorFactory.java      |  95 --------
 .../logging/log4j/core/impl/CoreDefaultBundle.java |  25 +-
 .../impl/CoreInstanceFactoryPostProcessor.java     |   3 -
 .../logging/log4j/core/impl/CoreProperties.java    |   5 +-
 .../logging/log4j/core/impl/Log4jLogEvent.java     |  11 +-
 .../logging/log4j/core/impl/Log4jProvider.java     |  15 ++
 .../log4j/core/impl/ReusableLogEventFactory.java   |  15 +-
 ...rovider.java => ScopedContextDataProvider.java} |  37 ++-
 .../log4j/core/impl/ThreadContextDataInjector.java | 271 ---------------------
 .../log4j/core/impl/ThreadContextDataProvider.java |  15 +-
 .../impl/internal/QueuedScopedContextProvider.java |  70 ++++++
 .../log4j/core/impl/internal/package-info.java     |  25 ++
 .../log4j/core/lookup/ContextMapLookup.java        |  15 +-
 .../log4j/core/util/ContextDataProvider.java       |  45 +++-
 .../log4j/perf/jmh/ThreadContextBenchmark.java     |  16 +-
 src/site/antora/modules/ROOT/nav.adoc              |   2 +
 .../ROOT/pages/manual/dependencyinjection.adoc     |   1 -
 .../modules/ROOT/pages/manual/extending.adoc       |   3 +-
 .../modules/ROOT/pages/manual/resource-logger.adoc |  88 +++++++
 .../modules/ROOT/pages/manual/scoped-context.adoc  | 116 +++++++++
 .../ROOT/pages/manual/systemproperties.adoc        |   7 -
 34 files changed, 589 insertions(+), 841 deletions(-)

diff --git a/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/AsyncLogger.java b/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/AsyncLogger.java
index cd430f94c0..5d0b2fc070 100644
--- a/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/AsyncLogger.java
+++ b/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/AsyncLogger.java
@@ -20,7 +20,6 @@ import java.util.List;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.Logger;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.ReusableLogEvent;
@@ -30,7 +29,7 @@ import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.config.Property;
 import org.apache.logging.log4j.core.config.ReliabilityStrategy;
-import org.apache.logging.log4j.core.impl.ContextDataFactory;
+import org.apache.logging.log4j.core.impl.ContextData;
 import org.apache.logging.log4j.core.time.Clock;
 import org.apache.logging.log4j.core.time.NanoClock;
 import org.apache.logging.log4j.kit.logger.AbstractLogger;
@@ -72,7 +71,6 @@ public class AsyncLogger extends Logger {
     // immediate inlining instead of waiting until they are designated "hot enough".
 
     private final Clock clock; // not reconfigurable
-    private final ContextDataInjector contextDataInjector; // not reconfigurable
 
     private final Recycler<RingBufferLogEventTranslator> translatorRecycler;
     private final AsyncLoggerDisruptor loggerDisruptor;
@@ -105,7 +103,6 @@ public class AsyncLogger extends Logger {
         includeLocation = privateConfig.loggerConfig.isIncludeLocation();
         nanoClock = configuration.getNanoClock();
         clock = configuration.getComponent(Clock.KEY);
-        contextDataInjector = configuration.getComponent(ContextDataInjector.KEY);
     }
 
     /*
@@ -214,7 +211,6 @@ public class AsyncLogger extends Logger {
                 location,
                 clock,
                 nanoClock,
-                contextDataInjector,
                 requiresLocation());
     }
 
@@ -255,7 +251,8 @@ public class AsyncLogger extends Logger {
 
     @SuppressWarnings("ForLoopReplaceableByForEach") // Avoid iterator allocation
     private void onPropertiesPresent(final ReusableLogEvent event, final List<Property> properties) {
-        final StringMap contextData = getContextData(event);
+        StringMap contextData = event.getContextData();
+        ContextData.addAll(contextData);
         for (int i = 0, size = properties.size(); i < size; i++) {
             final Property prop = properties.get(i);
             if (contextData.getValue(prop.getName()) != null) {
@@ -266,17 +263,6 @@ public class AsyncLogger extends Logger {
                     : prop.getValue();
             contextData.putValue(prop.getName(), value);
         }
-        event.setContextData(contextData);
-    }
-
-    private static StringMap getContextData(final ReusableLogEvent event) {
-        final StringMap contextData = event.getContextData();
-        if (contextData.isFrozen()) {
-            final StringMap temp = ContextDataFactory.createContextData();
-            temp.putAll(contextData);
-            return temp;
-        }
-        return contextData;
     }
 
     // package-protected for tests
diff --git a/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEvent.java b/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEvent.java
index c3a512a6c2..244e308174 100644
--- a/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEvent.java
+++ b/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEvent.java
@@ -120,7 +120,9 @@ public class RingBufferLogEvent implements ReusableLogEvent, ReusableMessage, Ch
         this.marker = aMarker;
         this.fqcn = theFqcn;
         this.location = aLocation;
-        this.contextData = mutableContextData;
+        if (mutableContextData != null) {
+            this.contextData = mutableContextData;
+        }
         this.contextStack = aContextStack;
         this.asyncLogger = anAsyncLogger;
         this.populated = true;
diff --git a/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEventTranslator.java b/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEventTranslator.java
index f4ad99fb63..f3dc6659f0 100644
--- a/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEventTranslator.java
+++ b/log4j-async-logger/src/main/java/org/apache/logging/log4j/async/logger/RingBufferLogEventTranslator.java
@@ -20,7 +20,7 @@ import com.lmax.disruptor.EventTranslator;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext.ContextStack;
-import org.apache.logging.log4j.core.ContextDataInjector;
+import org.apache.logging.log4j.core.impl.ContextData;
 import org.apache.logging.log4j.core.time.Clock;
 import org.apache.logging.log4j.core.time.NanoClock;
 import org.apache.logging.log4j.message.Message;
@@ -38,7 +38,6 @@ import org.jspecify.annotations.Nullable;
 @NullMarked
 public class RingBufferLogEventTranslator implements EventTranslator<RingBufferLogEvent> {
 
-    private ContextDataInjector contextDataInjector;
     private AsyncLogger asyncLogger;
     String loggerName;
     protected @Nullable Marker marker;
@@ -61,6 +60,9 @@ public class RingBufferLogEventTranslator implements EventTranslator<RingBufferL
     public void translateTo(final RingBufferLogEvent event, final long sequence) {
         try {
             final StringMap contextData = event.getContextData();
+            if (contextData != null) {
+                ContextData.addAll(contextData);
+            }
             // Compute location if necessary
             event.setValues(
                     asyncLogger,
@@ -70,9 +72,7 @@ public class RingBufferLogEventTranslator implements EventTranslator<RingBufferL
                     level,
                     message,
                     thrown,
-                    // config properties are taken care of in the EventHandler thread
-                    // in the AsyncLogger#actualAsyncLog method
-                    contextDataInjector.injectContextData(null, contextData),
+                    null,
                     contextStack,
                     threadId,
                     threadName,
@@ -90,7 +90,7 @@ public class RingBufferLogEventTranslator implements EventTranslator<RingBufferL
      * Release references held by this object to allow objects to be garbage-collected.
      */
     void clear() {
-        setBasicValues(null, null, null, null, null, null, null, null, null, null, null, null, false);
+        setBasicValues(null, null, null, null, null, null, null, null, null, null, null, false);
     }
 
     public void setBasicValues(
@@ -105,7 +105,6 @@ public class RingBufferLogEventTranslator implements EventTranslator<RingBufferL
             final @Nullable StackTraceElement location,
             final Clock clock,
             final NanoClock nanoClock,
-            final ContextDataInjector contextDataInjector,
             final boolean includeLocation) {
         this.asyncLogger = asyncLogger;
         this.loggerName = loggerName;
@@ -118,7 +117,6 @@ public class RingBufferLogEventTranslator implements EventTranslator<RingBufferL
         this.location = location;
         this.clock = clock;
         this.nanoClock = nanoClock;
-        this.contextDataInjector = contextDataInjector;
         this.requiresLocation = location == null && includeLocation;
     }
 
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LogEventFactoryTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LogEventFactoryTest.java
index 0799eeb2bb..73dc5ea1ec 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LogEventFactoryTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LogEventFactoryTest.java
@@ -23,6 +23,7 @@ import java.util.List;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.impl.ContextData;
 import org.apache.logging.log4j.core.impl.ContextDataFactory;
 import org.apache.logging.log4j.core.impl.Log4jLogEvent;
 import org.apache.logging.log4j.core.impl.LogEventFactory;
@@ -31,7 +32,7 @@ import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
 import org.apache.logging.log4j.core.test.junit.Named;
 import org.apache.logging.log4j.core.test.junit.TestBinding;
 import org.apache.logging.log4j.message.Message;
-import org.apache.logging.log4j.plugins.Inject;
+import org.apache.logging.log4j.util.StringMap;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -53,12 +54,6 @@ public class LogEventFactoryTest {
     }
 
     public static class TestLogEventFactory implements LogEventFactory {
-        private final ContextDataInjector injector;
-
-        @Inject
-        public TestLogEventFactory(final ContextDataInjector injector) {
-            this.injector = injector;
-        }
 
         @Override
         public LogEvent createEvent(
@@ -69,14 +64,20 @@ public class LogEventFactoryTest {
                 final Message data,
                 final List<Property> properties,
                 final Throwable t) {
+            StringMap contextData = ContextDataFactory.createContextData();
+            if (properties != null && !properties.isEmpty()) {
+                for (Property property : properties) {
+                    contextData.putValue(property.getName(), property.getValue());
+                }
+            }
+            ContextData.addAll(contextData);
             return Log4jLogEvent.newBuilder()
                     .setLoggerName("Test")
                     .setMarker(marker)
                     .setLoggerFqcn(fqcn)
                     .setLevel(level)
                     .setMessage(data)
-                    .setContextDataInjector(injector)
-                    .setContextData(injector.injectContextData(properties, ContextDataFactory.createContextData()))
+                    .setContextData(contextData)
                     .setThrown(t)
                     .build();
         }
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
deleted file mode 100644
index ee0a5a79e5..0000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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 static java.util.concurrent.Executors.newSingleThreadExecutor;
-import static org.apache.logging.log4j.ThreadContext.getThreadContextMap;
-import static org.apache.logging.log4j.core.impl.ContextDataInjectorFactory.createInjector;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.allOf;
-import static org.hamcrest.Matchers.empty;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.hasEntry;
-import static org.hamcrest.Matchers.hasKey;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.lang.reflect.Constructor;
-import java.util.Properties;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
-import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.ContextDataInjector;
-import org.apache.logging.log4j.core.ThreadContextTestAccess;
-import org.apache.logging.log4j.plugins.di.Key;
-import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
-import org.apache.logging.log4j.spi.ThreadContextMap;
-import org.apache.logging.log4j.util.PropertiesUtil;
-import org.apache.logging.log4j.util.ProviderUtil;
-import org.apache.logging.log4j.util.SortedArrayStringMap;
-import org.apache.logging.log4j.util.StringMap;
-import org.junit.After;
-import org.junit.Test;
-import org.junit.platform.commons.function.Try;
-import org.junit.platform.commons.support.ReflectionSupport;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameter;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class ThreadContextDataInjectorTest {
-
-    public static final String THREAD_LOCAL_MAP_CLASS_NAME =
-            "org.apache.logging.log4j.spi.CopyOnWriteSortedArrayThreadContextMap";
-    public static final String GARBAGE_FREE_MAP_CLASS_NAME =
-            "org.apache.logging.log4j.spi.GarbageFreeSortedArrayThreadContextMap";
-
-    @Parameters(name = "{1}")
-    public static Object[][] threadContextMapConstructorsAndReadOnlyNames() {
-        return new Object[][] {
-            {
-                (Function<Boolean, ThreadContextMap>) ThreadContextDataInjectorTest::createThreadLocalMap,
-                THREAD_LOCAL_MAP_CLASS_NAME
-            },
-            {
-                (Function<Boolean, ThreadContextMap>) ThreadContextDataInjectorTest::createGarbageFreeMap,
-                GARBAGE_FREE_MAP_CLASS_NAME
-            }
-        };
-    }
-
-    @Parameter(0)
-    public Function<Boolean, ThreadContextMap> threadContextMapConstructor;
-
-    @Parameter(1)
-    public String readOnlyThreadContextMapClassName;
-
-    @After
-    public void after() {
-        ThreadContext.remove("foo");
-        ThreadContext.remove("baz");
-    }
-
-    private void testContextDataInjector() {
-        final ReadOnlyThreadContextMap readOnlythreadContextMap = getThreadContextMap();
-        assertThat(
-                "thread context map class name",
-                (readOnlythreadContextMap == null)
-                        ? null
-                        : readOnlythreadContextMap.getClass().getName(),
-                is(equalTo(readOnlyThreadContextMapClassName)));
-
-        final ContextDataInjector contextDataInjector = createInjector();
-        final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap());
-
-        assertThat("thread context map", ThreadContext.getContext(), allOf(hasEntry("foo", "bar"), not(hasKey("baz"))));
-        assertThat("context map", stringMap.toMap(), allOf(hasEntry("foo", "bar"), not(hasKey("baz"))));
-
-        if (!stringMap.isFrozen()) {
-            stringMap.clear();
-            assertThat(
-                    "thread context map",
-                    ThreadContext.getContext(),
-                    allOf(hasEntry("foo", "bar"), not(hasKey("baz"))));
-            assertThat("context map", stringMap.toMap().entrySet(), is(empty()));
-        }
-
-        ThreadContext.put("foo", "bum");
-        ThreadContext.put("baz", "bam");
-
-        assertThat(
-                "thread context map",
-                ThreadContext.getContext(),
-                allOf(hasEntry("foo", "bum"), hasEntry("baz", "bam")));
-        if (stringMap.isFrozen()) {
-            assertThat("context map", stringMap.toMap(), allOf(hasEntry("foo", "bar"), not(hasKey("baz"))));
-        } else {
-            assertThat("context map", stringMap.toMap().entrySet(), is(empty()));
-        }
-    }
-
-    private void prepareThreadContext(final boolean isThreadContextMapInheritable) {
-        ((Log4jProvider) ProviderUtil.getProvider())
-                .instanceFactory.registerBinding(
-                        Key.forClass(ThreadContextMap.class),
-                        () -> threadContextMapConstructor.apply(isThreadContextMapInheritable));
-        ThreadContextTestAccess.init();
-        ThreadContext.remove("baz");
-        ThreadContext.put("foo", "bar");
-    }
-
-    @Test
-    public void testThreadContextImmutability() {
-        prepareThreadContext(false);
-        testContextDataInjector();
-    }
-
-    @Test
-    public void testInheritableThreadContextImmutability() throws Throwable {
-        prepareThreadContext(true);
-        try {
-            newSingleThreadExecutor().submit(this::testContextDataInjector).get();
-        } catch (ExecutionException ee) {
-            throw ee.getCause();
-        }
-    }
-
-    private static ThreadContextMap createThreadLocalMap(final boolean inheritable) {
-        final Try<Class<?>> loadedClass = ReflectionSupport.tryToLoadClass(THREAD_LOCAL_MAP_CLASS_NAME);
-        final Class<? extends ThreadContextMap> mapClass =
-                assertDoesNotThrow(loadedClass::get).asSubclass(ThreadContextMap.class);
-        return newInstanceWithPropertiesUtilArg(mapClass, inheritable);
-    }
-
-    private static ThreadContextMap createGarbageFreeMap(final boolean inheritable) {
-        final Try<Class<?>> loadedClass = ReflectionSupport.tryToLoadClass(GARBAGE_FREE_MAP_CLASS_NAME);
-        final Class<? extends ThreadContextMap> mapClass =
-                assertDoesNotThrow(loadedClass::get).asSubclass(ThreadContextMap.class);
-        return newInstanceWithPropertiesUtilArg(mapClass, inheritable);
-    }
-
-    private static ThreadContextMap newInstanceWithPropertiesUtilArg(
-            final Class<? extends ThreadContextMap> mapClass, final boolean inheritable) {
-        final Constructor<? extends ThreadContextMap> constructor =
-                assertDoesNotThrow(() -> mapClass.getDeclaredConstructor(PropertiesUtil.class));
-        assertTrue(constructor.trySetAccessible(), () -> "Unable to access constructor for " + mapClass);
-        final Properties properties = new Properties();
-        properties.setProperty("log4j2.isThreadContextMapInheritable", Boolean.toString(inheritable));
-        final PropertiesUtil propertiesUtil = new PropertiesUtil(properties);
-        return assertDoesNotThrow(() -> constructor.newInstance(propertiesUtil));
-    }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java
index 0ac46f7aad..5cca5d3e20 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/ContextDataProviderTest.java
@@ -20,6 +20,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -27,9 +30,9 @@ 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.impl.ThreadContextDataInjector;
 import org.apache.logging.log4j.core.test.TestConstants;
 import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -39,10 +42,16 @@ public class ContextDataProviderTest {
 
     private static Logger logger;
     private static ListAppender appender;
+    private static final String serviceDefn = TestContextDataProvider.class.getName();
+    private static final String serviceFile =
+            "target/test-classes/META-INF/services/" + ContextDataProvider.class.getName();
 
     @BeforeAll
-    public static void beforeClass() {
-        ThreadContextDataInjector.contextDataProviders.add(new TestContextDataProvider());
+    public static void beforeClass() throws Exception {
+
+        Path path = Paths.get(serviceFile);
+        byte[] strToBytes = serviceDefn.getBytes();
+        Files.write(path, strToBytes);
         System.setProperty(TestConstants.CONFIGURATION_FILE, "log4j-contextData.xml");
         final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
         logger = loggerContext.getLogger(ContextDataProviderTest.class.getName());
@@ -50,6 +59,12 @@ public class ContextDataProviderTest {
         assertNotNull(appender, "No List appender");
     }
 
+    @AfterAll
+    public static void afterClass() throws Exception {
+        Path path = Paths.get(serviceFile);
+        Files.delete(path);
+    }
+
     @Test
     public void testContextProvider() {
         ThreadContext.put("loginId", "jdoe");
@@ -59,7 +74,7 @@ public class ContextDataProviderTest {
         assertTrue(messages.get(0).contains("testKey=testValue"), "Context data missing");
     }
 
-    private static class TestContextDataProvider implements ContextDataProvider {
+    public static class TestContextDataProvider implements ContextDataProvider {
 
         @Override
         public Map<String, String> supplyContextData() {
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
deleted file mode 100644
index 4e352bd6c5..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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;
-
-import java.util.List;
-import org.apache.logging.log4j.core.config.Property;
-import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
-import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
-import org.apache.logging.log4j.plugins.di.Key;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
-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>
- * In some asynchronous models, work may be delegated to several threads, while conceptually this work shares the same
- * context. In such models, storing context data in {@code ThreadLocal} variables is not convenient or desirable.
- * Users can configure the {@code ContextDataInjectorFactory} to provide custom {@code ContextDataInjector} objects,
- * in order to initialize log events with context data from any arbitrary context.
- * </p><p>
- * When providing a custom {@code ContextDataInjector}, be aware that the {@code ContextDataInjectorFactory} may be
- * invoked multiple times and the various components in Log4j that need access to context data may each have their own
- * instance of {@code ContextDataInjector}.
- * This includes the object(s) that populate log events, but also various lookups and filters that look at
- * context data to determine whether an event should be logged.
- * </p><p>
- * Implementors should take particular note of how the different methods in the interface have different thread-safety
- * guarantees to enable optimal performance.
- * </p>
- *
- * @see StringMap
- * @see ReadOnlyStringMap
- * @see ContextDataInjectorFactory
- * @see org.apache.logging.log4j.ThreadContext
- * @see ThreadContextDataInjector
- * @since 2.7
- */
-public interface ContextDataInjector {
-    Key<ContextDataInjector> KEY = new Key<>() {};
-
-    /**
-     * Returns a {@code StringMap} object initialized with the specified properties and the appropriate
-     * context data. The returned value may be the specified parameter or a different object.
-     * <p>
-     * This method will be called for each log event to initialize its context data and implementors should take
-     * care to make this method as performant as possible while preserving at least the following thread-safety
-     * guarantee.
-     * </p><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><p>
-     * Example implementation:
-     * </p>
-     * <pre>
-     * public StringMap injectContextData(List<Property> properties, StringMap reusable) {
-     *     if (properties == null || properties.isEmpty()) {
-     *         // assume context data is stored in a copy-on-write data structure that is safe to pass to another thread
-     *         return (StringMap) rawContextData();
-     *     }
-     *     // first copy configuration properties into the result
-     *     ThreadContextDataInjector.copyProperties(properties, reusable);
-     *
-     *     // then copy context data key-value pairs (may overwrite configuration properties)
-     *     reusable.putAll(rawContextData());
-     *     return reusable;
-     * }
-     * </pre>
-     *
-     * @param properties Properties from the log4j configuration to be added to the resulting ReadOnlyStringMap. May be
-     *          {@code null} or empty
-     * @param reusable a {@code StringMap} instance that may be reused to avoid creating temporary objects
-     * @return a {@code StringMap} instance initialized with the specified properties and the appropriate
-     *          context data. The returned value may be the specified parameter or a different object.
-     * @see ThreadContextDataInjector#copyProperties(List, StringMap)
-     */
-    StringMap injectContextData(final List<Property> properties, final StringMap reusable);
-
-    /**
-     * Returns a {@code ReadOnlyStringMap} object reflecting the current state of the context. Configuration properties
-     * are not included in the result.
-     * <p>
-     * This method may be called multiple times for each log event by Filters and Lookups and implementors should take
-     * care to make this method as performant as possible while preserving at least the following thread-safety
-     * guarantee.
-     * </p><p>
-     * Thread-safety note: The returned object can only be safely used <em>in the current thread</em>. Changes in the
-     * underlying context may or may not be reflected in the returned object, depending on the context data source and
-     * the implementation of this method. It is not safe to pass the returned object to another thread.
-     * </p>
-     * @return a {@code ReadOnlyStringMap} object reflecting the current state of the context, may not return {@code null}
-     */
-    ReadOnlyStringMap rawContextData();
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/LogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/LogEvent.java
index 8c855d54de..1b473e3e17 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/LogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/LogEvent.java
@@ -22,6 +22,7 @@ import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.impl.MementoLogEvent;
 import org.apache.logging.log4j.core.impl.ThrowableProxy;
 import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.jspecify.annotations.Nullable;
@@ -63,11 +64,10 @@ public interface LogEvent {
      * Context data (also known as Mapped Diagnostic Context or MDC) is data that is set by the application to be
      * included in all subsequent log events. The default source for context data is the {@link ThreadContext} (and
      * <a href="https://logging.apache.org/log4j/2.x/manual/configuration.html#PropertySubstitution">properties</a>
-     * configured on the Logger that logged the event), but users can configure a custom {@link ContextDataInjector}
+     * configured on the Logger that logged the event), but users can configure a custom {@link ContextDataProvider}
      * to inject key-value pairs from any arbitrary source.
      *
      * @return the {@code ReadOnlyStringMap} object holding context data key-value pairs
-     * @see ContextDataInjector
      * @see ThreadContext
      * @since 2.7
      */
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
index 8b51750056..281fe09bd4 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.core.filter;
 
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Supplier;
@@ -24,16 +25,16 @@ import java.util.stream.Stream;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.impl.ContextData;
 import org.apache.logging.log4j.core.impl.ContextDataFactory;
-import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
+import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
 import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.plugins.Configurable;
-import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAttribute;
 import org.apache.logging.log4j.plugins.PluginElement;
@@ -44,8 +45,8 @@ import org.apache.logging.log4j.util.StringMap;
 
 /**
  * Compares against a log level that is associated with a context value. By default the context is the
- * {@link ThreadContext}, but users may {@linkplain ContextDataInjectorFactory configure} a custom
- * {@link ContextDataInjector} which obtains context data from some other source.
+ * {@link ThreadContext}, but users may {@linkplain ContextDataProvider configure} a custom
+ * {@link ContextDataProvider} which obtains context data from some other source.
  */
 @Configurable(elementType = Filter.ELEMENT_TYPE, printObject = true)
 @Plugin
@@ -62,7 +63,6 @@ public final class DynamicThresholdFilter extends AbstractFilter {
         private String key;
         private KeyValuePair[] pairs;
         private Level defaultThreshold;
-        private ContextDataInjector contextDataInjector;
 
         public Builder setKey(@PluginAttribute final String key) {
             this.key = key;
@@ -79,30 +79,19 @@ public final class DynamicThresholdFilter extends AbstractFilter {
             return this;
         }
 
-        @Inject
-        public Builder setContextDataInjector(final ContextDataInjector contextDataInjector) {
-            this.contextDataInjector = contextDataInjector;
-            return this;
-        }
-
         @Override
         public DynamicThresholdFilter get() {
-            if (contextDataInjector == null) {
-                contextDataInjector = ContextDataInjectorFactory.createInjector();
-            }
             if (defaultThreshold == null) {
                 defaultThreshold = Level.ERROR;
             }
             final Map<String, Level> map = Stream.of(pairs)
                     .collect(Collectors.toMap(KeyValuePair::getKey, pair -> Level.toLevel(pair.getValue())));
-            return new DynamicThresholdFilter(
-                    key, map, defaultThreshold, getOnMatch(), getOnMismatch(), contextDataInjector);
+            return new DynamicThresholdFilter(key, map, defaultThreshold, getOnMatch(), getOnMismatch());
         }
     }
 
     private final Level defaultThreshold;
     private final String key;
-    private final ContextDataInjector injector;
     private final Map<String, Level> levelMap;
 
     private DynamicThresholdFilter(
@@ -110,8 +99,7 @@ public final class DynamicThresholdFilter extends AbstractFilter {
             final Map<String, Level> pairs,
             final Level defaultLevel,
             final Result onMatch,
-            final Result onMismatch,
-            final ContextDataInjector injector) {
+            final Result onMismatch) {
         super(onMatch, onMismatch);
         // ContextDataFactory looks up a property. The Spring PropertySource may log which will cause recursion.
         // By initializing the ContextDataFactory here recursion will be prevented.
@@ -123,7 +111,6 @@ public final class DynamicThresholdFilter extends AbstractFilter {
         this.key = key;
         this.levelMap = pairs;
         this.defaultThreshold = defaultLevel;
-        this.injector = injector;
     }
 
     @Override
@@ -198,7 +185,9 @@ public final class DynamicThresholdFilter extends AbstractFilter {
     }
 
     private ReadOnlyStringMap currentContextData() {
-        return injector.rawContextData();
+        Map<String, String> contextData = new HashMap<>();
+        ContextData.addAll(contextData);
+        return new JdkMapAdapterStringMap(contextData, true);
     }
 
     @Override
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
index 396033ed21..470d9a23ca 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
@@ -31,7 +31,6 @@ import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.Logger;
@@ -346,7 +345,6 @@ public class MutableThreadContextMapFilter extends AbstractFilter {
                                 .setOperator("or")
                                 .setOnMatch(getOnMatch())
                                 .setOnMismatch(getOnMismatch())
-                                .setContextDataInjector(configuration.getComponent(ContextDataInjector.KEY))
                                 .get();
                     } else {
                         filter = new NoOpFilter();
@@ -382,7 +380,6 @@ public class MutableThreadContextMapFilter extends AbstractFilter {
                         .setOperator("or")
                         .setOnMatch(getOnMatch())
                         .setOnMismatch(getOnMismatch())
-                        .setContextDataInjector(configuration.getComponent(ContextDataInjector.KEY))
                         .get();
                 LOGGER.info("Filter configuration was updated: {}", filter.toString());
                 for (FilterConfigUpdateListener listener : listeners) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
index 3ea4b12ead..130e9d9960 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
@@ -24,16 +24,15 @@ import java.util.Map;
 import java.util.function.Supplier;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.Filter;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.impl.ContextData;
 import org.apache.logging.log4j.core.impl.ContextDataFactory;
-import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
+import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
 import org.apache.logging.log4j.core.util.KeyValuePair;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.plugins.Configurable;
-import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAliases;
 import org.apache.logging.log4j.plugins.PluginAttribute;
@@ -54,18 +53,13 @@ import org.apache.logging.log4j.util.StringMap;
 @PerformanceSensitive("allocation")
 public class ThreadContextMapFilter extends MapFilter {
 
-    private final ContextDataInjector injector;
     private final String key;
     private final String value;
 
     private final boolean useMap;
 
     public ThreadContextMapFilter(
-            final Map<String, List<String>> pairs,
-            final boolean oper,
-            final Result onMatch,
-            final Result onMismatch,
-            final ContextDataInjector injector) {
+            final Map<String, List<String>> pairs, final boolean oper, final Result onMatch, final Result onMismatch) {
         super(pairs, oper, onMatch, onMismatch);
         // ContextDataFactory looks up a property. The Spring PropertySource may log which will cause recursion.
         // By initializing the ContextDataFactory here recursion will be prevented.
@@ -91,7 +85,6 @@ public class ThreadContextMapFilter extends MapFilter {
             this.value = null;
             this.useMap = true;
         }
-        this.injector = injector;
     }
 
     @Override
@@ -134,7 +127,9 @@ public class ThreadContextMapFilter extends MapFilter {
     }
 
     private ReadOnlyStringMap currentContextData() {
-        return injector.rawContextData();
+        Map<String, String> contextData = new HashMap<>();
+        ContextData.addAll(contextData);
+        return new JdkMapAdapterStringMap(contextData, true);
     }
 
     @Override
@@ -286,7 +281,6 @@ public class ThreadContextMapFilter extends MapFilter {
     public static class Builder extends AbstractFilterBuilder<Builder> implements Supplier<ThreadContextMapFilter> {
         private KeyValuePair[] pairs;
         private String operator;
-        private ContextDataInjector contextDataInjector;
 
         public Builder setPairs(@Required @PluginElement final KeyValuePair[] pairs) {
             this.pairs = pairs;
@@ -298,12 +292,6 @@ public class ThreadContextMapFilter extends MapFilter {
             return this;
         }
 
-        @Inject
-        public Builder setContextDataInjector(final ContextDataInjector contextDataInjector) {
-            this.contextDataInjector = contextDataInjector;
-            return this;
-        }
-
         @Override
         public ThreadContextMapFilter get() {
             if (pairs == null || pairs.length == 0) {
@@ -336,9 +324,7 @@ public class ThreadContextMapFilter extends MapFilter {
                 return null;
             }
             final boolean isAnd = operator == null || !operator.equalsIgnoreCase("or");
-            final ContextDataInjector injector =
-                    contextDataInjector != null ? contextDataInjector : ContextDataInjectorFactory.createInjector();
-            return new ThreadContextMapFilter(map, isAnd, getOnMatch(), getOnMismatch(), injector);
+            return new ThreadContextMapFilter(map, isAnd, getOnMatch(), getOnMismatch());
         }
     }
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextData.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextData.java
new file mode 100644
index 0000000000..df951a2e9f
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextData.java
@@ -0,0 +1,107 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.ServiceLoaderUtil;
+import org.apache.logging.log4j.util.StringMap;
+
+/**
+ * General purpose utility class for accessing data accessible through ContextDataProviders.
+ */
+public final class ContextData {
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+    /**
+     * ContextDataProviders loaded via OSGi.
+     */
+    public static Collection<ContextDataProvider> contextDataProviders = new ConcurrentLinkedDeque<>();
+
+    private static final List<ContextDataProvider> SERVICE_PROVIDERS = getServiceProviders();
+
+    private ContextData() {}
+
+    private static List<ContextDataProvider> getServiceProviders() {
+        final List<ContextDataProvider> providers = new ArrayList<>();
+        ServiceLoaderUtil.safeStream(
+                        ContextDataProvider.class,
+                        ServiceLoader.load(ContextDataProvider.class, ContextData.class.getClassLoader()),
+                        LOGGER)
+                .forEach(providers::add);
+        return Collections.unmodifiableList(providers);
+    }
+
+    public static void addProvider(ContextDataProvider provider) {
+        contextDataProviders.add(provider);
+    }
+
+    private static List<ContextDataProvider> getProviders() {
+        final List<ContextDataProvider> providers =
+                new ArrayList<>(contextDataProviders.size() + SERVICE_PROVIDERS.size());
+        providers.addAll(contextDataProviders);
+        providers.addAll(SERVICE_PROVIDERS);
+        return providers;
+    }
+
+    public static int size() {
+        final List<ContextDataProvider> providers = getProviders();
+        final AtomicInteger count = new AtomicInteger(0);
+        providers.forEach((provider) -> count.addAndGet(provider.size()));
+        return count.get();
+    }
+
+    /**
+     * Populates the provided StringMap with data from the Context.
+     * @param stringMap the StringMap to contain the results.
+     */
+    public static void addAll(StringMap stringMap) {
+        final List<ContextDataProvider> providers = getProviders();
+        providers.forEach((provider) -> provider.addAll(stringMap));
+    }
+
+    /**
+     * Populates the provided Map with data from the Context.
+     * @param map the Map to contain the results.
+     * @return the Map. Useful for chaining operations.
+     */
+    public static Map<String, String> addAll(Map<String, String> map) {
+        final List<ContextDataProvider> providers = getProviders();
+        providers.forEach((provider) -> provider.addAll(map));
+        return map;
+    }
+
+    public static String getValue(String key) {
+        List<ContextDataProvider> providers = getProviders();
+        for (ContextDataProvider provider : providers) {
+            String value = provider.get(key);
+            if (value != null) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataFactory.java
index 205f1b6f50..bfc16c69f3 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataFactory.java
@@ -19,9 +19,9 @@ package org.apache.logging.log4j.core.impl;
 import java.lang.reflect.Constructor;
 import java.util.Map;
 import java.util.Map.Entry;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.impl.CoreProperties.LogEventProperties;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
 import org.apache.logging.log4j.kit.env.PropertyEnvironment;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.SortedArrayStringMap;
@@ -29,7 +29,7 @@ import org.apache.logging.log4j.util.StringMap;
 
 /**
  * Factory for creating the StringMap instances used to initialize LogEvents' {@linkplain LogEvent#getContextData()
- * context data}. When context data is {@linkplain ContextDataInjector injected} into the log event, these StringMap
+ * context data}. When context data is {@linkplain ContextDataProvider injected} into the log event, these StringMap
  * instances may be either populated with key-value pairs from the context, or completely replaced altogether.
  * <p>
  *     By default returns {@code SortedArrayStringMap} objects. Can be configured by setting system property
@@ -39,7 +39,6 @@ import org.apache.logging.log4j.util.StringMap;
  * </p>
  *
  * @see LogEvent#getContextData()
- * @see ContextDataInjector
  * @see SortedArrayStringMap
  * @since 2.7
  */
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
deleted file mode 100644
index 17241113f7..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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.ContextDataInjector;
-import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.impl.CoreProperties.LogEventProperties;
-import org.apache.logging.log4j.kit.env.PropertyEnvironment;
-import org.apache.logging.log4j.spi.CopyOnWrite;
-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;
-
-/**
- * Factory for ContextDataInjectors. Returns a new {@code ContextDataInjector} instance based on the value of system
- * property {@code log4j2.ContextDataInjector}. Users may use this system property to specify the fully qualified class
- * name of a class that implements the {@code ContextDataInjector} interface.
- * If no value was specified this factory method returns one of the injectors defined in
- * {@code ThreadContextDataInjector}.
- *
- * @see ContextDataInjector
- * @see ReadOnlyStringMap
- * @see ThreadContextDataInjector
- * @see LogEvent#getContextData()
- * @since 2.7
- */
-public class ContextDataInjectorFactory {
-
-    /**
-     * Returns a new {@code ContextDataInjector} instance based on the value of system property
-     * {@code log4j2.ContextDataInjector}. If no value was specified this factory method returns one of the
-     * {@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>
-     * When providing a custom {@code ContextDataInjector}, be aware that this method may be invoked multiple times by
-     * the various components in Log4j that need access to context data.
-     * This includes the object(s) that populate log events, but also various lookups and filters that look at
-     * context data to determine whether an event should be logged.
-     * </p>
-     *
-     * @return a ContextDataInjector that populates the {@code ReadOnlyStringMap} of all {@code LogEvent} objects
-     * @see LogEvent#getContextData()
-     * @see ContextDataInjector
-     */
-    public static ContextDataInjector createInjector() {
-        try {
-            final Class<? extends ContextDataInjector> injector = PropertyEnvironment.getGlobal()
-                    .getProperty(LogEventProperties.class)
-                    .contextData()
-                    .injector();
-            if (injector != null) {
-                return LoaderUtil.newInstanceOf(injector);
-            }
-        } catch (final ReflectiveOperationException e) {
-            StatusLogger.getLogger().warn("Could not create ContextDataInjector: {}", e.getMessage(), e);
-        }
-        return createDefaultInjector();
-    }
-
-    private static ContextDataInjector createDefaultInjector() {
-        final ReadOnlyThreadContextMap threadContextMap = ThreadContext.getThreadContextMap();
-
-        // note: map may be null (if legacy custom ThreadContextMap was installed by user)
-        if (threadContextMap == null) {
-            return new ThreadContextDataInjector.ForDefaultThreadContextMap(); // for non StringMap-based context maps
-        }
-        if (threadContextMap instanceof CopyOnWrite) {
-            return new ThreadContextDataInjector.ForCopyOnWriteThreadContextMap();
-        }
-        return new ThreadContextDataInjector.ForGarbageFreeThreadContextMap();
-    }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreDefaultBundle.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreDefaultBundle.java
index 85f58a55c0..4e9e2f47e1 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreDefaultBundle.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreDefaultBundle.java
@@ -24,8 +24,6 @@ import java.util.function.Supplier;
 import java.util.stream.Stream;
 import org.apache.logging.log4j.Level;
 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.async.AsyncQueueFullPolicy;
 import org.apache.logging.log4j.core.async.AsyncQueueFullPolicyFactory;
 import org.apache.logging.log4j.core.config.ConfigurationFactory;
@@ -60,10 +58,8 @@ import org.apache.logging.log4j.plugins.Namespace;
 import org.apache.logging.log4j.plugins.SingletonFactory;
 import org.apache.logging.log4j.plugins.condition.ConditionalOnMissingBinding;
 import org.apache.logging.log4j.plugins.di.ConfigurableInstanceFactory;
-import org.apache.logging.log4j.spi.CopyOnWrite;
 import org.apache.logging.log4j.spi.LoggerContextFactory;
 import org.apache.logging.log4j.spi.Provider;
-import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.ServiceLoaderUtil;
 
@@ -76,7 +72,6 @@ import org.apache.logging.log4j.util.ServiceLoaderUtil;
  * @see ConfigurationFactory
  * @see MergeStrategy
  * @see InterpolatorFactory
- * @see ContextDataInjector
  * @see LogEventFactory
  * @see StrSubstitutor
  */
@@ -146,27 +141,11 @@ public final class CoreDefaultBundle {
         return new DummyNanoClock();
     }
 
-    @SingletonFactory
-    @ConditionalOnMissingBinding
-    public ContextDataInjector defaultContextDataInjector() {
-        final ReadOnlyThreadContextMap threadContextMap = ThreadContext.getThreadContextMap();
-        if (threadContextMap != null) {
-            return threadContextMap instanceof CopyOnWrite
-                    ? new ThreadContextDataInjector.ForCopyOnWriteThreadContextMap()
-                    : new ThreadContextDataInjector.ForGarbageFreeThreadContextMap();
-        }
-        // for non StringMap-based context maps
-        return new ThreadContextDataInjector.ForDefaultThreadContextMap();
-    }
-
     @SingletonFactory
     @ConditionalOnMissingBinding
     public LogEventFactory reusableLogEventFactory(
-            final ContextDataInjector injector,
-            final Clock clock,
-            final NanoClock nanoClock,
-            final RecyclerFactory recyclerFactory) {
-        return new ReusableLogEventFactory(injector, clock, nanoClock, recyclerFactory);
+            final Clock clock, final NanoClock nanoClock, final RecyclerFactory recyclerFactory) {
+        return new ReusableLogEventFactory(clock, nanoClock, recyclerFactory);
     }
 
     @SingletonFactory
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreInstanceFactoryPostProcessor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreInstanceFactoryPostProcessor.java
index 33957707df..ccc0c06ee6 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreInstanceFactoryPostProcessor.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreInstanceFactoryPostProcessor.java
@@ -20,7 +20,6 @@ import static org.apache.logging.log4j.plugins.di.Key.forClass;
 
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceProvider;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.LoggerContext;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.ConfigurationFactory;
@@ -70,8 +69,6 @@ public class CoreInstanceFactoryPostProcessor implements ConfigurableInstanceFac
             final ConfigurableInstanceFactory factory, final PropertyEnvironment env) {
         final LogEventProperties logEvent = env.getProperty(LogEventProperties.class);
         registerIfPresent(factory, LogEventFactory.class, logEvent.factory());
-        registerIfPresent(
-                factory, ContextDataInjector.class, logEvent.contextData().injector());
 
         final StatusLoggerProperties statusLogger = env.getProperty(StatusLoggerProperties.class);
         factory.registerBinding(Constants.STATUS_LOGGER_LEVEL_KEY, statusLogger::level);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreProperties.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreProperties.java
index 27188eba77..205f639114 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreProperties.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/CoreProperties.java
@@ -19,7 +19,6 @@ package org.apache.logging.log4j.core.impl;
 import java.net.URI;
 import java.nio.file.Path;
 import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.config.ConfigurationFactory;
 import org.apache.logging.log4j.core.config.ReliabilityStrategy;
 import org.apache.logging.log4j.core.config.composite.MergeStrategy;
@@ -165,10 +164,8 @@ public final class CoreProperties {
 
     /**
      * @param type The {@link StringMap} class to use for context data.
-     * @param injector The {@link ContextDataInjector} to use to retrieve context data.
      */
-    public record ContextDataProperties(
-            @Nullable Class<? extends StringMap> type, @Nullable Class<? extends ContextDataInjector> injector) {}
+    public record ContextDataProperties(@Nullable Class<? extends StringMap> type) {}
 
     @Log4jProperty(name = "loggerContext")
     public record LoggerContextProperties(
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
index 0b59f4f950..c5350a0a9d 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
@@ -20,7 +20,6 @@ import java.util.Objects;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.ReusableLogEvent;
 import org.apache.logging.log4j.core.config.LoggerConfig;
@@ -84,7 +83,6 @@ public class Log4jLogEvent implements LogEvent {
         private boolean endOfBatch = false;
         private long nanoTime;
         private Clock clock;
-        private ContextDataInjector contextDataInjector;
 
         public Builder() {
             initDefaultContextData();
@@ -238,11 +236,6 @@ public class Log4jLogEvent implements LogEvent {
             return this;
         }
 
-        public Builder setContextDataInjector(final ContextDataInjector contextDataInjector) {
-            this.contextDataInjector = contextDataInjector;
-            return this;
-        }
-
         @Override
         public Log4jLogEvent build() {
             initTimeFields();
@@ -279,8 +272,8 @@ public class Log4jLogEvent implements LogEvent {
         }
 
         private void initDefaultContextData() {
-            contextDataInjector = ContextDataInjectorFactory.createInjector();
-            contextData = contextDataInjector.injectContextData(null, ContextDataFactory.createContextData());
+            contextData = ContextDataFactory.createContextData();
+            ContextData.addAll(contextData);
         }
     }
 
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 ae80ba2d3c..be342cc3f2 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
@@ -18,7 +18,9 @@ package org.apache.logging.log4j.core.impl;
 
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.ServiceLoader;
 import org.apache.logging.log4j.core.impl.CoreProperties.ThreadContextProperties;
+import org.apache.logging.log4j.core.impl.internal.QueuedScopedContextProvider;
 import org.apache.logging.log4j.kit.env.PropertyEnvironment;
 import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.di.ConfigurableInstanceFactory;
@@ -26,8 +28,11 @@ import org.apache.logging.log4j.plugins.di.DI;
 import org.apache.logging.log4j.plugins.di.Key;
 import org.apache.logging.log4j.spi.LoggerContextFactory;
 import org.apache.logging.log4j.spi.Provider;
+import org.apache.logging.log4j.spi.ScopedContextProvider;
 import org.apache.logging.log4j.spi.ThreadContextMap;
+import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Constants;
+import org.apache.logging.log4j.util.ServiceLoaderUtil;
 
 /**
  * Binding for the Log4j API.
@@ -75,4 +80,14 @@ public class Log4jProvider extends Provider {
     public ThreadContextMap getThreadContextMapInstance() {
         return instanceFactory.getInstance(Key.forClass(ThreadContextMap.class));
     }
+
+    @Override
+    public ScopedContextProvider getScopedContextProvider() {
+        return ServiceLoaderUtil.safeStream(
+                        ScopedContextProvider.class,
+                        ServiceLoader.load(ScopedContextProvider.class),
+                        StatusLogger.getLogger())
+                .findFirst()
+                .orElse(QueuedScopedContextProvider.INSTANCE);
+    }
 }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
index e70d9faed2..f0e9a4815a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
@@ -20,7 +20,6 @@ import java.util.List;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.ReusableLogEvent;
 import org.apache.logging.log4j.core.config.Property;
@@ -38,18 +37,13 @@ import org.apache.logging.log4j.plugins.Inject;
  */
 public class ReusableLogEventFactory implements LogEventFactory {
 
-    private final ContextDataInjector injector;
     private final Clock clock;
     private final NanoClock nanoClock;
     private final Recycler<MutableLogEvent> recycler;
 
     @Inject
     public ReusableLogEventFactory(
-            final ContextDataInjector injector,
-            final Clock clock,
-            final NanoClock nanoClock,
-            final RecyclerFactory recyclerFactory) {
-        this.injector = injector;
+            final Clock clock, final NanoClock nanoClock, final RecyclerFactory recyclerFactory) {
         this.clock = clock;
         this.nanoClock = nanoClock;
         this.recycler = recyclerFactory.create(() -> {
@@ -120,7 +114,12 @@ public class ReusableLogEventFactory implements LogEventFactory {
         result.initTime(clock, nanoClock);
         result.setThrown(t);
         result.setSource(location);
-        result.setContextData(injector.injectContextData(properties, result.getContextData()));
+        if (properties != null && !properties.isEmpty()) {
+            for (Property property : properties) {
+                result.getContextData().putValue(property.getName(), property.getValue());
+            }
+        }
+        ContextData.addAll(result.getContextData());
         result.setContextStack(
                 ThreadContext.getDepth() == 0 ? ThreadContext.EMPTY_STACK : ThreadContext.cloneStack()); // mutable copy
         result.setThreadName(Thread.currentThread().getName());
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/ScopedContextDataProvider.java
similarity index 51%
copy from log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java
copy to log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
index a20216c755..948d9c04b6 100644
--- 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/ScopedContextDataProvider.java
@@ -18,24 +18,47 @@ package org.apache.logging.log4j.core.impl;
 
 import aQute.bnd.annotation.Resolution;
 import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
-import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.util.ContextDataProvider;
-import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.spi.ScopedContextProvider;
+import org.apache.logging.log4j.util.ProviderUtil;
 
 /**
- * ContextDataProvider for ThreadContext data.
+ * ContextDataProvider for {@code Map<String, String>} data.
+ * @since 2.24.0
  */
 @ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
-public class ThreadContextDataProvider implements ContextDataProvider {
+public class ScopedContextDataProvider implements ContextDataProvider {
+
+    private final ScopedContextProvider scopedContext =
+            ProviderUtil.getProvider().getScopedContextProvider();
+
+    @Override
+    public String get(final String key) {
+        return scopedContext.getString(key);
+    }
 
     @Override
     public Map<String, String> supplyContextData() {
-        return ThreadContext.getImmutableContext();
+        final Map<String, ?> contextMap = scopedContext.getContextMap();
+        if (!contextMap.isEmpty()) {
+            final Map<String, String> map = new HashMap<>();
+            contextMap.forEach((key, value) -> map.put(key, value.toString()));
+            return map;
+        } else {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public int size() {
+        return scopedContext.getContextMap().size();
     }
 
     @Override
-    public StringMap supplyStringMap() {
-        return ThreadContext.getThreadContextMap().getReadOnlyContextData();
+    public void addAll(final Map<String, String> map) {
+        scopedContext.getContextMap().forEach((key, value) -> map.put(key, String.valueOf(value)));
     }
 }
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
deleted file mode 100644
index 7b36d1b762..0000000000
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * 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 aQute.bnd.annotation.Cardinality;
-import aQute.bnd.annotation.spi.ServiceConsumer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-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.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.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.ServiceLoaderUtil;
-import org.apache.logging.log4j.util.StringMap;
-
-/**
- * {@code ThreadContextDataInjector} contains a number of strategies for copying key-value pairs from the various
- * {@code ThreadContext} map implementations into a {@code StringMap}. In the case of duplicate keys,
- * thread context values overwrite configuration {@code Property} values.
- * <p>
- * These are the default {@code ContextDataInjector} objects returned by the {@link ContextDataInjectorFactory}.
- * </p>
- *
- * @see org.apache.logging.log4j.ThreadContext
- * @see Property
- * @see ReadOnlyStringMap
- * @see ContextDataInjector
- * @see ContextDataInjectorFactory
- * @since 2.7
- */
-@ServiceConsumer(value = ContextDataProvider.class, cardinality = Cardinality.MULTIPLE)
-public class ThreadContextDataInjector {
-
-    /**
-     * ContextDataProviders loaded externally.
-     */
-    public static Collection<ContextDataProvider> contextDataProviders = new ConcurrentLinkedDeque<>();
-
-    private static final List<ContextDataProvider> SERVICE_PROVIDERS = getServiceProviders();
-
-    private static List<ContextDataProvider> getServiceProviders() {
-        final List<ContextDataProvider> providers = new ArrayList<>();
-        final ServiceLoader<ContextDataProvider> serviceLoader =
-                ServiceLoader.load(ContextDataProvider.class, ThreadContextDataInjector.class.getClassLoader());
-        ServiceLoaderUtil.safeStream(ContextDataProvider.class, serviceLoader, StatusLogger.getLogger())
-                .forEachOrdered(provider -> {
-                    if (providers.stream().noneMatch((p) -> p.getClass().isAssignableFrom(provider.getClass()))) {
-                        providers.add(provider);
-                    }
-                });
-        return Collections.unmodifiableList(providers);
-    }
-
-    /**
-     * Default {@code ContextDataInjector} for the legacy {@code Map<String, String>}-based ThreadContext (which is
-     * also the ThreadContext implementation used for web applications).
-     * <p>
-     * This injector always puts key-value pairs into the specified reusable StringMap.
-     */
-    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
-         * @return a {@code StringMap} combining configuration properties with thread context data
-         */
-        @Override
-        public StringMap injectContextData(final List<Property> props, final StringMap ignore) {
-
-            final Map<String, String> copy;
-
-            if (providers.size() == 1) {
-                copy = providers.get(0).supplyContextData();
-            } else {
-                copy = new HashMap<>();
-                for (final 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 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())) {
-                // 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);
-            }
-            // 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 = new JdkMapAdapterStringMap(new HashMap<>(copy), false);
-            for (int i = 0; i < props.size(); i++) {
-                final Property prop = props.get(i);
-                if (!copy.containsKey(prop.getName())) {
-                    result.putValue(prop.getName(), prop.getValue());
-                }
-            }
-            result.freeze();
-            return result;
-        }
-
-        private static JdkMapAdapterStringMap frozenStringMap(final Map<String, String> copy) {
-            return new JdkMapAdapterStringMap(copy, true);
-        }
-
-        @Override
-        public ReadOnlyStringMap rawContextData() {
-            final ReadOnlyThreadContextMap map = ThreadContext.getThreadContextMap();
-            if (map != null) {
-                return map.getReadOnlyContextData();
-            }
-            // note: default ThreadContextMap is null
-            final Map<String, String> copy = ThreadContext.getImmutableContext();
-            return copy.isEmpty()
-                    ? ContextDataFactory.emptyFrozenContextData()
-                    : new JdkMapAdapterStringMap(copy, true);
-        }
-    }
-
-    /**
-     * The {@code ContextDataInjector} used when the ThreadContextMap implementation is a garbage-free
-     * StringMap-based data structure.
-     * <p>
-     * 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.
-         *
-         * @param props list of configuration properties, may be {@code null}
-         * @param reusable a {@code StringMap} instance that may be reused to avoid creating temporary objects
-         * @return a {@code StringMap} combining configuration properties with thread context data
-         */
-        @Override
-        public StringMap injectContextData(final List<Property> props, final StringMap reusable) {
-            // When the ThreadContext is garbage-free, we must copy its key-value pairs into the specified reusable
-            // 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);
-            for (int i = 0; i < providers.size(); ++i) {
-                reusable.putAll(providers.get(i).supplyStringMap());
-            }
-            return reusable;
-        }
-
-        @Override
-        public ReadOnlyStringMap rawContextData() {
-            return ThreadContext.getThreadContextMap().getReadOnlyContextData();
-        }
-    }
-
-    /**
-     * The {@code ContextDataInjector} used when the ThreadContextMap implementation is a copy-on-write
-     * StringMap-based data structure.
-     * <p>
-     * 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
-     * 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
-         * specified reusable StringMap.
-         *
-         * @param props list of configuration properties, may be {@code null}
-         * @param ignore 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) {
-            // 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.
-            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 == null ? 0 : props.size();
-            final 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(count);
-            copyProperties(props, result);
-            for (final StringMap map : maps) {
-                result.putAll(map);
-            }
-            return result;
-        }
-
-        @Override
-        public ReadOnlyStringMap rawContextData() {
-            return ThreadContext.getThreadContextMap().getReadOnlyContextData();
-        }
-    }
-
-    /**
-     * Copies key-value pairs from the specified property list into the specified {@code StringMap}.
-     *
-     * @param properties list of configuration properties, may be {@code null}
-     * @param result the {@code StringMap} object to add the key-values to. Must be non-{@code null}.
-     */
-    public static void copyProperties(final List<Property> properties, final StringMap result) {
-        if (properties != null) {
-            for (int i = 0; i < properties.size(); i++) {
-                final Property prop = properties.get(i);
-                result.putValue(prop.getName(), prop.getValue());
-            }
-        }
-    }
-
-    private static List<ContextDataProvider> getProviders() {
-        final List<ContextDataProvider> providers =
-                new ArrayList<>(contextDataProviders.size() + SERVICE_PROVIDERS.size());
-        providers.addAll(contextDataProviders);
-        providers.addAll(SERVICE_PROVIDERS);
-        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
index a20216c755..5361b6222d 100644
--- 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
@@ -21,7 +21,6 @@ import aQute.bnd.annotation.spi.ServiceProvider;
 import java.util.Map;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.core.util.ContextDataProvider;
-import org.apache.logging.log4j.util.StringMap;
 
 /**
  * ContextDataProvider for ThreadContext data.
@@ -29,13 +28,23 @@ import org.apache.logging.log4j.util.StringMap;
 @ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
 public class ThreadContextDataProvider implements ContextDataProvider {
 
+    @Override
+    public String get(String key) {
+        return ThreadContext.get(key);
+    }
+
     @Override
     public Map<String, String> supplyContextData() {
         return ThreadContext.getImmutableContext();
     }
 
     @Override
-    public StringMap supplyStringMap() {
-        return ThreadContext.getThreadContextMap().getReadOnlyContextData();
+    public int size() {
+        return ThreadContext.getContext().size();
+    }
+
+    @Override
+    public void addAll(Map<String, String> map) {
+        map.putAll(ThreadContext.getContext());
     }
 }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java
new file mode 100644
index 0000000000..626b9c0255
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java
@@ -0,0 +1,70 @@
+/*
+ * 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.internal;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Optional;
+import org.apache.logging.log4j.spi.AbstractScopedContextProvider;
+import org.apache.logging.log4j.spi.ScopedContextProvider;
+
+public class QueuedScopedContextProvider extends AbstractScopedContextProvider {
+
+    public static final ScopedContextProvider INSTANCE = new QueuedScopedContextProvider();
+
+    private final ThreadLocal<Deque<Instance>> scopedContext = new ThreadLocal<>();
+
+    /**
+     * Returns an immutable Map containing all the key/value pairs as Object objects.
+     * @return An immutable copy of the Map at the current scope.
+     */
+    @Override
+    protected Optional<Instance> getContext() {
+        final Deque<Instance> stack = scopedContext.get();
+        return stack != null ? Optional.of(stack.getFirst()) : Optional.empty();
+    }
+
+    /**
+     * Add the ScopeContext.
+     * @param context The ScopeContext.
+     */
+    @Override
+    protected void addScopedContext(final MapInstance context) {
+        Deque<Instance> stack = scopedContext.get();
+        if (stack == null) {
+            stack = new ArrayDeque<>();
+            scopedContext.set(stack);
+        }
+        stack.addFirst(context);
+    }
+
+    /**
+     * Remove the top ScopeContext.
+     */
+    @Override
+    protected void removeScopedContext() {
+        final Deque<Instance> stack = scopedContext.get();
+        if (stack != null) {
+            if (!stack.isEmpty()) {
+                stack.removeFirst();
+            }
+            if (stack.isEmpty()) {
+                scopedContext.remove();
+            }
+        }
+    }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
new file mode 100644
index 0000000000..1f5f55c8ba
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+/**
+ * Log4j 2 private implementation classes.
+ */
+@Export
+@Version("3.0.0")
+package org.apache.logging.log4j.core.impl.internal;
+
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
index f672d1106d..25e48b23f5 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
@@ -17,23 +17,20 @@
 package org.apache.logging.log4j.core.lookup;
 
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
+import org.apache.logging.log4j.core.impl.ContextData;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
 import org.apache.logging.log4j.plugins.Plugin;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
 
 /**
  * Looks up keys from the context. By default this is the {@link ThreadContext}, but users may
- * {@linkplain ContextDataInjectorFactory configure} a custom {@link ContextDataInjector} which obtains context data
+ * {@linkplain ContextDataProvider configure} a custom {@link ContextDataProvider} which obtains context data
  * from some other source.
  */
 @Lookup
 @Plugin("ctx")
 public class ContextMapLookup implements StrLookup {
 
-    private final ContextDataInjector injector = ContextDataInjectorFactory.createInjector();
-
     /**
      * Looks up the value from the ThreadContext Map.
      * @param key  the key to be looked up, may be null
@@ -41,11 +38,7 @@ public class ContextMapLookup implements StrLookup {
      */
     @Override
     public String lookup(final String key) {
-        return currentContextData().getValue(key);
-    }
-
-    private ReadOnlyStringMap currentContextData() {
-        return injector.rawContextData();
+        return ContextData.getValue(key);
     }
 
     /**
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
index 8ac63b6858..bc12d1b253 100644
--- 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
@@ -17,7 +17,6 @@
 package org.apache.logging.log4j.core.util;
 
 import java.util.Map;
-import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
 import org.apache.logging.log4j.util.StringMap;
 
 /**
@@ -25,6 +24,15 @@ import org.apache.logging.log4j.util.StringMap;
  */
 public interface ContextDataProvider {
 
+    /**
+     * Returns the key for a value from the context data.
+     * @param key the key to locate.
+     * @return the value or null if it is not found.
+     */
+    default String get(String key) {
+        return null;
+    }
+
     /**
      * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
      * <p>
@@ -36,14 +44,33 @@ public interface ContextDataProvider {
     Map<String, String> supplyContextData();
 
     /**
-     * Returns the context data as a StringMap.
-     * <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 the context data in a StringMap.
+     * Returns the number of items in this context.
+     * @return the number of items in the context.
+     */
+    default int size() {
+        Map<String, String> contextMap = supplyContextData();
+        return contextMap != null ? contextMap.size() : 0;
+    }
+
+    /**
+     * Add all the keys in the current context to the provided Map.
+     * @param map the StringMap to add the keys and values to.
+     */
+    default void addAll(Map<String, String> map) {
+        Map<String, String> contextMap = supplyContextData();
+        if (contextMap != null) {
+            map.putAll(contextMap);
+        }
+    }
+
+    /**
+     * Add all the keys in the current context to the provided StringMap.
+     * @param map the StringMap to add the keys and values to.
      */
-    default StringMap supplyStringMap() {
-        return new JdkMapAdapterStringMap(supplyContextData(), true);
+    default void addAll(StringMap map) {
+        Map<String, String> contextMap = supplyContextData();
+        if (contextMap != null) {
+            contextMap.forEach(map::putValue);
+        }
     }
 }
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java
index b6f7d1c814..77a5d34d7c 100644
--- a/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java
@@ -25,9 +25,8 @@ import java.util.Random;
 import java.util.concurrent.TimeUnit;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.ThreadContextBenchmarkAccess;
-import org.apache.logging.log4j.core.ContextDataInjector;
 import org.apache.logging.log4j.core.config.Property;
-import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
+import org.apache.logging.log4j.core.impl.ContextData;
 import org.apache.logging.log4j.core.test.TestConstants;
 import org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
 import org.apache.logging.log4j.spi.CopyOnWriteOpenHashMapThreadContextMap;
@@ -99,7 +98,6 @@ public class ThreadContextBenchmark {
     private String[] values;
     private List<Property> propertyList;
 
-    private ContextDataInjector injector;
     private StringMap reusableContextData;
 
     @Setup
@@ -109,9 +107,6 @@ public class ThreadContextBenchmark {
                 IMPLEMENTATIONS.get(threadContextMapAlias).getName());
         ThreadContextBenchmarkAccess.init();
 
-        injector = ContextDataInjectorFactory.createInjector();
-        System.out.println(threadContextMapAlias + ": Injector = " + injector);
-
         reusableContextData =
                 threadContextMapAlias.contains("Array") ? new SortedArrayStringMap() : new OpenHashStringMap<>();
 
@@ -166,13 +161,18 @@ public class ThreadContextBenchmark {
     @Benchmark
     public StringMap injectWithoutProperties() {
         reusableContextData.clear();
-        return injector.injectContextData(null, reusableContextData);
+        ContextData.addAll(reusableContextData);
+        return reusableContextData;
     }
 
     @Benchmark
     public StringMap injectWithProperties() {
         reusableContextData.clear();
-        return injector.injectContextData(propertyList, reusableContextData);
+        for (Property property : propertyList) {
+            reusableContextData.putValue(property.getName(), property.getValue());
+        }
+        ContextData.addAll(reusableContextData);
+        return reusableContextData;
     }
 
     @Benchmark
diff --git a/src/site/antora/modules/ROOT/nav.adoc b/src/site/antora/modules/ROOT/nav.adoc
index 5f9fbb5cb2..9da894dbc9 100644
--- a/src/site/antora/modules/ROOT/nav.adoc
+++ b/src/site/antora/modules/ROOT/nav.adoc
@@ -34,6 +34,8 @@
 *** xref:manual/eventlogging.adoc[]
 *** xref:manual/messages.adoc[]
 *** xref:manual/thread-context.adoc[]
+*** xref:manual/scoped-context.adoc[]
+*** xref:manual/resource-logger.adoc[]
 ** xref:manual/configuration.adoc[]
 ** xref:manual/usage.adoc[]
 ** xref:manual/cloud.adoc[]
diff --git a/src/site/antora/modules/ROOT/pages/manual/dependencyinjection.adoc b/src/site/antora/modules/ROOT/pages/manual/dependencyinjection.adoc
index e6686c541a..0a1e35d6a9 100644
--- a/src/site/antora/modules/ROOT/pages/manual/dependencyinjection.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/dependencyinjection.adoc
@@ -138,7 +138,6 @@ Each post-processor is given the `ConfigurableInstanceFactory` being initialized
 The default callback sets up bindings for the following keys if none have been registered.
 Some of these bindings were previously configured through various system properties which are supported via the default callback and its default bindings, though they can be directly registered via custom callbacks with a negative order value.
 
-* `org.apache.logging.log4j.core.ContextDataInjector`
 * `org.apache.logging.log4j.core.config.ConfigurationFactory`
 * `org.apache.logging.log4j.core.config.composite.MergeStrategy`
 * `org.apache.logging.log4j.core.impl.LogEventFactory`
diff --git a/src/site/antora/modules/ROOT/pages/manual/extending.adoc b/src/site/antora/modules/ROOT/pages/manual/extending.adoc
index 3d88ca16ea..c2a9c45313 100644
--- a/src/site/antora/modules/ROOT/pages/manual/extending.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/extending.adoc
@@ -589,8 +589,7 @@ ListAppender list2 = ListAppender.newBuilder().setName("List1").setEntryPerNewLi
 
 The link:../javadoc/log4j-core/org/apache/logging/log4j/core/util/ContextDataProvider.html[`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 
-link:../javadoc/log4j-core/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.html[`ThreadContextDataInjector`]
+additional key-value pairs into the LogEvent's context data. Log4j
 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
diff --git a/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc b/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
new file mode 100644
index 0000000000..674de056aa
--- /dev/null
+++ b/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
@@ -0,0 +1,88 @@
+////
+    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.
+////
+
+= Resource Logging
+The link:../log4j-api/apidocs/org/apache/logging/log4j/ResourceLogger.html[`ResourceLogger`]
+is available in Log4j API releases 2.24.0 and greater.
+
+A `ResourceLogger` is a special kind of Logger that:
+
+ * is a regular class member variable that will be garbage collected along with the class instance.
+ * can provide a Map of key/value pairs of data associate with the resource (the class instance)
+that will be include in every record logged from the class.
+
+The Resource Logger still uses a "regular" Logger. That Logger can be explicitly declared or encapsulated
+inside the Resource Logger.
+
+[source,java]
+----
+
+     private class User {
+
+        private final String loginId;
+        private final String role;
+        private int loginAttempts;
+        private final ResourceLogger logger;
+
+        public User(final String loginId, final String role) {
+            this.loginId = loginId;
+            this.role = role;
+            logger = ResourceLogger.newBuilder()
+                .withClass(this.getClass())
+                .withSupplier(new UserSupplier())
+                .build();
+        }
+
+        public void login() throws Exception {
+            ++loginAttempts;
+            try {
+                authenticator.authenticate(loginId);
+                logger.info("Login succeeded");
+            } catch (Exception ex) {
+                logger.warn("Failed login");
+                throw ex;
+            }
+        }
+
+
+        private class UserSupplier implements Supplier<Map<String, String>> {
+
+            public Map<String, String> get() {
+                Map<String, String> map = new HashMap<>();
+                map.put("LoginId", loginId);
+                map.put("Role", role);
+                map.put("Count", Integer.toString(loginAttempts));
+                return map;
+            }
+        }
+    }
+
+----
+
+With the PatternLayout configured with a pattern of
+
+----
+%X %m%n
+----
+
+and a loginId of testUser and a role of Admin, after a successful login would result in a log message of
+
+----
+{LoginId=testUser, Role=Admin, Count=1} Login succeeded
+----
+
+Every logging call is wrapped in a ScopedContext and populated by the supplier configured on the ResourceLogger, which is called when generating every log event. This allows values, such as counters, to be updated and the log event will contain the actual value at the time the event was logged.
diff --git a/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc b/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc
new file mode 100644
index 0000000000..f80bc6f671
--- /dev/null
+++ b/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc
@@ -0,0 +1,116 @@
+////
+    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.
+////
+
+= Scoped Context
+The link:../log4j-api/apidocs/org/apache/logging/log4j/ScopedContext.html[`ScopedContext`]
+is available in Log4j API releases 2.24.0 and greater.
+
+The `ScopedContext` is similar to the ThreadContextMap in that it allows key/value pairs to be included
+in many log events. However, the pairs in a `ScopedContext` are only available to
+application code and log events running within the scope of the `ScopeContext` object.
+
+The `ScopeContext` is essentially a builder that allows key/value pairs to be added to it
+prior to invoking a method. The key/value pairs are available to any code running within
+that method and will be included in all logging events as if they were part of the `ThreadContextMap`.
+
+ScopedContext is immutable. Each invocation of the `where` method returns a new ScopedContext.Instance
+with the specified key/value pair added to those defined in previous ScopedContexts.
+
+[source,java]
+----
+ScopedContext.where("id", UUID.randomUUID())
+    .where("ipAddress", request.getRemoteAddr())
+    .where("loginId", session.getAttribute("loginId"))
+    .where("hostName", request.getServerName())
+    .run(new Worker());
+
+private class Worker implements Runnable {
+    private static final Logger LOGGER = LogManager.getLogger(Worker.class);
+
+    public void run() {
+        LOGGER.debug("Performing work");
+        String loginId = ScopedContext.get("loginId");
+    }
+}
+
+----
+
+The values in the ScopedContext can be any Java object. However, objects stored in the
+context Map will be converted to Strings when stored in a LogEvent. To aid in
+this Objects may implement the Renderable interface which provides a `render` method
+to format the object. By default, objects will have their toString() method called
+if they do not implement the Renderable interface.
+
+Note that in the example above `UUID.randomUUID()` returns a UUID. By default, when it is
+included in LogEvents its toString() method will be used.
+
+== Thread Support
+
+ScopedContext provides support for passing the ScopedContext and the ThreadContext to
+child threads by way of an ExecutorService. For example, the following will create a
+ScopedContext and pass it to a child thread.
+
+[source,java]
+----
+BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
+ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, workQueue);
+Future<?> future = ScopedContext.where("id", UUID.randomUUID())
+    .where("ipAddress", request.getRemoteAddr())
+    .where("loginId", session.getAttribute("loginId"))
+    .where("hostName", request.getServerName())
+    .run(executorService, new Worker());
+try {
+    future.get();
+} catch (ExecutionException ex) {
+    logger.warn("Exception in worker thread: {}", ex.getMessage());
+}
+
+private class Worker implements Runnable {
+    private static final Logger LOGGER = LogManager.getLogger(Worker.class);
+
+    public void run() {
+        LOGGER.debug("Performing work");
+        String loginId = ScopedContext.get("loginId");
+    }
+}
+
+----
+
+ScopeContext also supports call methods in addition to run methods so the called functions can
+directly return values.
+
+== Nested ScopedContexts
+
+ScopedContexts may be nested. Becuase ScopedContexts are immutable the `where` method may
+be called on the current ScopedContext from within the run or call methods to append new
+key/value pairs. In addition, when passing a single key/value pair the run or call method
+may be combined with a where method as shown below.
+
+
+[source,java]
+----
+        ScopedContext.runWhere("key1", "value1", () -> {
+            assertThat(ScopedContext.get("key1"), equalTo("value1"));
+            ScopedContext.where("key2", "value2").run(() -> {
+                assertThat(ScopedContext.get("key1"), equalTo("value1"));
+                assertThat(ScopedContext.get("key2"), equalTo("value2"));
+            });
+        });
+
+----
+
+ScopedContexts ALWAYS inherit the key/value pairs from their parent scope. key/value pairs may be removed from the context by passing a null value with the key. Note that where methods that accept a Map MUST NOT include null keys or values in the map.
\ No newline at end of file
diff --git a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
index cc38388028..87cc59e698 100644
--- a/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/systemproperties.adoc
@@ -764,13 +764,6 @@ must have a default constructor.
 |Prints a stacktrace to the status logger at DEBUG level when the
 LoggerContext is started. For debug purposes.
 
-|ThreadContext
-|contextDataInjector
-|log4j2.contextDataInjector, LOG4J_CONTEXT_DATA_INJECTOR
-|System/Application
-|
-|Fully specified class name of a custom `ContextDataInjector` implementation class.
-
 |TransportSecurity
 |keyStoreLocation
 |log4j2.keyStoreLocation, LOG4J_KEY_STORE_LOCATION