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

pkarwasz pushed a commit to branch ScopedContext-replace-with-interface
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit c28029d0a8f89850f804bac7f1e1760d04766ce6
Author: Ralph Goers <[email protected]>
AuthorDate: Wed Mar 27 13:46:40 2024 -0700

    Add ScopedContext and ResourceLogger
---
 .../org/apache/logging/log4j/test/TestLogger.java  |   6 +
 .../apache/logging/log4j/ResourceLoggerTest.java   | 136 +++++
 .../apache/logging/log4j/ScopedContextTest.java    | 154 ++++++
 .../log4j/message/ParameterizedMapMessageTest.java |  77 +++
 .../log4j/message/ParameterizedMessageTest.java    | 303 -----------
 .../org/apache/logging/log4j/ResourceLogger.java   | 276 ++++++++++
 .../org/apache/logging/log4j/ScopedContext.java    | 558 +++++++++++++++++++++
 .../log4j/internal/ScopedContextAnchor.java        |  69 +++
 .../log4j/message/ParameterizedMapMessage.java     |  38 ++
 .../message/ParameterizedMapMessageFactory.java    | 216 ++++++++
 .../apache/logging/log4j/message/package-info.java |   2 +-
 .../org/apache/logging/log4j/package-info.java     |   2 +-
 .../apache/logging/log4j/simple/SimpleLogger.java  |   7 +-
 .../apache/logging/log4j/ResourceLoggerTest.java   | 158 ++++++
 .../logging/log4j/core/ScopedContextTest.java      |  72 +++
 log4j-core-test/src/test/resources/log4j-list2.xml |  31 ++
 log4j-core-test/src/test/resources/log4j-map.xml   |  34 ++
 .../log4j/core/impl/ScopedContextDataProvider.java |  50 ++
 .../core/impl/{ => internal}/package-info.java     |   4 +-
 .../logging/log4j/core/impl/package-info.java      |   2 +-
 src/changelog/.2.x.x/add_scoped_context.xml        |   9 +
 src/site/_release-notes/_2.x.x.adoc                |   1 +
 src/site/asciidoc/docs.adoc                        |   2 +
 src/site/asciidoc/manual/resource-logger.adoc      |  93 ++++
 src/site/asciidoc/manual/scoped-context.adoc       | 118 +++++
 25 files changed, 2108 insertions(+), 310 deletions(-)

diff --git 
a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java 
b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java
index e95f1e9bb6..ff9c6f01c0 100644
--- a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/TestLogger.java
@@ -27,6 +27,7 @@ import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.ParameterizedMapMessage;
 import org.apache.logging.log4j.spi.AbstractLogger;
 
 /**
@@ -85,6 +86,11 @@ public class TestLogger extends AbstractLogger {
             sb.append(mdc);
             sb.append(' ');
         }
+        if (message instanceof ParameterizedMapMessage) {
+            sb.append(" Resource data: ");
+            sb.append(((ParameterizedMapMessage) 
message).getData().toString());
+            sb.append(' ');
+        }
         final Object[] params = message.getParameters();
         final Throwable t;
         if (throwable == null
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
new file mode 100644
index 0000000000..f706b0a7dd
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.test.TestLogger;
+import org.apache.logging.log4j.test.TestLoggerContextFactory;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Class Description goes here.
+ */
+public class ResourceLoggerTest {
+    @BeforeAll
+    public static void beforeAll() {
+        System.setProperty("log4j2.loggerContextFactory", 
TestLoggerContextFactory.class.getName());
+    }
+
+    @Test
+    public void testFactory() throws Exception {
+        Connection connection = new Connection("Test", "dummy");
+        connection.useConnection();
+        MapSupplier mapSupplier = new MapSupplier(connection);
+        ResourceLogger logger = ResourceLogger.newBuilder()
+                .withClass(this.getClass())
+                .withSupplier(mapSupplier)
+                .build();
+        logger.debug("Hello, {}", "World");
+        Logger log = LogManager.getLogger(this.getClass().getName());
+        assertTrue(log instanceof TestLogger);
+        TestLogger testLogger = (TestLogger) log;
+        List<String> events = testLogger.getEntries();
+        assertThat(events, hasSize(1));
+        assertThat(events.get(0), containsString("Name=Test"));
+        assertThat(events.get(0), containsString("Type=dummy"));
+        assertThat(events.get(0), containsString("Count=1"));
+        assertThat(events.get(0), containsString("Hello, World"));
+        events.clear();
+        connection.useConnection();
+        logger.debug("Used the connection");
+        assertThat(events.get(0), containsString("Count=2"));
+        assertThat(events.get(0), containsString("Used the connection"));
+        events.clear();
+        connection = new Connection("NewConnection", "fiber");
+        connection.useConnection();
+        mapSupplier = new MapSupplier(connection);
+        logger = ResourceLogger.newBuilder().withSupplier(mapSupplier).build();
+        logger.debug("Connection: {}", "NewConnection");
+        assertThat(events, hasSize(1));
+        assertThat(events.get(0), containsString("Name=NewConnection"));
+        assertThat(events.get(0), containsString("Type=fiber"));
+        assertThat(events.get(0), containsString("Count=1"));
+        assertThat(events.get(0), containsString("Connection: NewConnection"));
+        events.clear();
+    }
+
+    private static class MapSupplier implements Supplier<Map<String, String>> {
+
+        private final Connection connection;
+
+        public MapSupplier(final Connection connection) {
+            this.connection = connection;
+        }
+
+        @Override
+        public Map<String, String> get() {
+            Map<String, String> map = new HashMap<>();
+            map.put("Name", connection.name);
+            map.put("Type", connection.type);
+            map.put("Count", Long.toString(connection.getCounter()));
+            return map;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return o instanceof MapSupplier;
+        }
+
+        @Override
+        public int hashCode() {
+            return 77;
+        }
+    }
+
+    private static class Connection {
+
+        private final String name;
+        private final String type;
+        private final AtomicLong counter = new AtomicLong(0);
+
+        public Connection(final String name, final String type) {
+            this.name = name;
+            this.type = type;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public long getCounter() {
+            return counter.get();
+        }
+
+        public void useConnection() {
+            counter.incrementAndGet();
+        }
+    }
+}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java
new file mode 100644
index 0000000000..d9ba5872e6
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.jupiter.api.Test;
+
+public class ScopedContextTest {
+
+    @Test
+    public void testScope() {
+        ScopedContext.where("key1", "Log4j2").run(() -> 
assertThat(ScopedContext.get("key1"), equalTo("Log4j2")));
+        ScopedContext.where("key1", "value1").run(() -> {
+            assertThat(ScopedContext.get("key1"), equalTo("value1"));
+            ScopedContext.where("key2", "value2").run(() -> {
+                assertThat(ScopedContext.get("key1"), equalTo("value1"));
+                assertThat(ScopedContext.get("key2"), equalTo("value2"));
+            });
+        });
+    }
+
+    @Test
+    public void testRunWhere() {
+        ScopedContext.runWhere("key1", "Log4j2", () -> 
assertThat(ScopedContext.get("key1"), equalTo("Log4j2")));
+        ScopedContext.runWhere("key1", "value1", () -> {
+            assertThat(ScopedContext.get("key1"), equalTo("value1"));
+            ScopedContext.runWhere("key2", "value2", () -> {
+                assertThat(ScopedContext.get("key1"), equalTo("value1"));
+                assertThat(ScopedContext.get("key2"), equalTo("value2"));
+            });
+        });
+    }
+
+    @Test
+    public void testRunThreads() throws Exception {
+        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
+        ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, 
TimeUnit.SECONDS, workQueue);
+        final long id = Thread.currentThread().getId();
+        final AtomicLong counter = new AtomicLong(0);
+        ScopedContext.runWhere("key1", "Log4j2", () -> {
+            assertThat(ScopedContext.get("key1"), equalTo("Log4j2"));
+            Future<?> future = ScopedContext.runWhere("key2", "value2", 
executorService, () -> {
+                assertNotEquals(Thread.currentThread().getId(), id);
+                assertThat(ScopedContext.get("key1"), equalTo("Log4j2"));
+                counter.incrementAndGet();
+            });
+            try {
+                future.get();
+                assertTrue(future.isDone());
+                assertEquals(1, counter.get());
+            } catch (Exception ex) {
+                fail("Failed with " + ex.getMessage());
+            }
+        });
+    }
+
+    @Test
+    public void testThreads() throws Exception {
+        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
+        ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, 
TimeUnit.SECONDS, workQueue);
+        final long id = Thread.currentThread().getId();
+        final AtomicLong counter = new AtomicLong(0);
+        ScopedContext.where("key1", "Log4j2").run(() -> {
+            assertThat(ScopedContext.get("key1"), equalTo("Log4j2"));
+            Future<?> future = ScopedContext.where("key2", 
"value2").run(executorService, () -> {
+                assertNotEquals(Thread.currentThread().getId(), id);
+                assertThat(ScopedContext.get("key1"), equalTo("Log4j2"));
+                counter.incrementAndGet();
+            });
+            try {
+                future.get();
+                assertTrue(future.isDone());
+                assertEquals(1, counter.get());
+            } catch (Exception ex) {
+                fail("Failed with " + ex.getMessage());
+            }
+        });
+    }
+
+    @Test
+    public void testThreadException() throws Exception {
+        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
+        final AtomicBoolean exceptionCaught = new AtomicBoolean(false);
+        ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, 
TimeUnit.SECONDS, workQueue);
+        long id = Thread.currentThread().getId();
+        ScopedContext.runWhere("key1", "Log4j2", () -> {
+            assertThat(ScopedContext.get("key1"), equalTo("Log4j2"));
+            Future<?> future = ScopedContext.where("key2", 
"value2").run(executorService, () -> {
+                assertNotEquals(Thread.currentThread().getId(), id);
+                throw new NullPointerException("On purpose NPE");
+            });
+            try {
+                future.get();
+            } catch (ExecutionException ex) {
+                assertThat(ex.getMessage(), 
equalTo("java.lang.NullPointerException: On purpose NPE"));
+                return;
+            } catch (Exception ex) {
+                fail("Failed with " + ex.getMessage());
+            }
+            fail("No exception caught");
+        });
+    }
+
+    @Test
+    public void testThreadCall() throws Exception {
+        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
+        ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, 
TimeUnit.SECONDS, workQueue);
+        final long id = Thread.currentThread().getId();
+        final AtomicInteger counter = new AtomicInteger(0);
+        int returnVal = ScopedContext.callWhere("key1", "Log4j2", () -> {
+            assertThat(ScopedContext.get("key1"), equalTo("Log4j2"));
+            Future<Integer> future = ScopedContext.callWhere("key2", "value2", 
executorService, () -> {
+                assertNotEquals(Thread.currentThread().getId(), id);
+                assertThat(ScopedContext.get("key1"), equalTo("Log4j2"));
+                return counter.incrementAndGet();
+            });
+            Integer val = future.get();
+            assertTrue(future.isDone());
+            assertEquals(1, counter.get());
+            return val;
+        });
+        assertThat(returnVal, equalTo(1));
+    }
+}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMapMessageTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMapMessageTest.java
new file mode 100644
index 0000000000..a560570846
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMapMessageTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.message;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.logging.log4j.test.ListStatusListener;
+import org.apache.logging.log4j.test.junit.UsingStatusListener;
+import org.junit.jupiter.api.Test;
+
+@UsingStatusListener
+class ParameterizedMapMessageTest {
+
+    final ListStatusListener statusListener;
+
+    ParameterizedMapMessageTest(ListStatusListener statusListener) {
+        this.statusListener = statusListener;
+    }
+
+    @Test
+    void testNoArgs() {
+        final String testMsg = "Test message {}";
+        ParameterizedMessage msg = new ParameterizedMessage(testMsg, 
(Object[]) null);
+        String result = msg.getFormattedMessage();
+        assertThat(result).isEqualTo(testMsg);
+        final Object[] array = null;
+        msg = new ParameterizedMessage(testMsg, array, null);
+        result = msg.getFormattedMessage();
+        assertThat(result).isEqualTo(testMsg);
+    }
+
+    @Test
+    void testZeroLength() {
+        final String testMsg = "";
+        ParameterizedMessage msg = new ParameterizedMessage(testMsg, new 
Object[] {"arg"});
+        String result = msg.getFormattedMessage();
+        assertThat(result).isEqualTo(testMsg);
+        final Object[] array = null;
+        msg = new ParameterizedMessage(testMsg, array, null);
+        result = msg.getFormattedMessage();
+        assertThat(result).isEqualTo(testMsg);
+    }
+
+    @Test
+    void testOneCharLength() {
+        final String testMsg = "d";
+        ParameterizedMessage msg = new ParameterizedMessage(testMsg, new 
Object[] {"arg"});
+        String result = msg.getFormattedMessage();
+        assertThat(result).isEqualTo(testMsg);
+        final Object[] array = null;
+        msg = new ParameterizedMessage(testMsg, array, null);
+        result = msg.getFormattedMessage();
+        assertThat(result).isEqualTo(testMsg);
+    }
+
+    @Test
+    void testFormat3StringArgs() {
+        final String testMsg = "Test message {}{} {}";
+        final String[] args = {"a", "b", "c"};
+        final String result = ParameterizedMessage.format(testMsg, args);
+        assertThat(result).isEqualTo("Test message ab c");
+    }
+}
diff --git 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageTest.java
 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageTest.java
deleted file mode 100644
index 4bd5df91be..0000000000
--- 
a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageTest.java
+++ /dev/null
@@ -1,303 +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.message;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-import java.math.BigDecimal;
-import java.util.List;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.status.StatusData;
-import org.apache.logging.log4j.test.ListStatusListener;
-import org.apache.logging.log4j.test.junit.Mutable;
-import org.apache.logging.log4j.test.junit.SerialUtil;
-import org.apache.logging.log4j.test.junit.UsingStatusListener;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-@UsingStatusListener
-class ParameterizedMessageTest {
-
-    final ListStatusListener statusListener;
-
-    ParameterizedMessageTest(ListStatusListener statusListener) {
-        this.statusListener = statusListener;
-    }
-
-    @Test
-    void testNoArgs() {
-        final String testMsg = "Test message {}";
-        ParameterizedMessage msg = new ParameterizedMessage(testMsg, 
(Object[]) null);
-        String result = msg.getFormattedMessage();
-        assertThat(result).isEqualTo(testMsg);
-        final Object[] array = null;
-        msg = new ParameterizedMessage(testMsg, array, null);
-        result = msg.getFormattedMessage();
-        assertThat(result).isEqualTo(testMsg);
-    }
-
-    @Test
-    void testZeroLength() {
-        final String testMsg = "";
-        ParameterizedMessage msg = new ParameterizedMessage(testMsg, new 
Object[] {"arg"});
-        String result = msg.getFormattedMessage();
-        assertThat(result).isEqualTo(testMsg);
-        final Object[] array = null;
-        msg = new ParameterizedMessage(testMsg, array, null);
-        result = msg.getFormattedMessage();
-        assertThat(result).isEqualTo(testMsg);
-    }
-
-    @Test
-    void testOneCharLength() {
-        final String testMsg = "d";
-        ParameterizedMessage msg = new ParameterizedMessage(testMsg, new 
Object[] {"arg"});
-        String result = msg.getFormattedMessage();
-        assertThat(result).isEqualTo(testMsg);
-        final Object[] array = null;
-        msg = new ParameterizedMessage(testMsg, array, null);
-        result = msg.getFormattedMessage();
-        assertThat(result).isEqualTo(testMsg);
-    }
-
-    @Test
-    void testFormat3StringArgs() {
-        final String testMsg = "Test message {}{} {}";
-        final String[] args = {"a", "b", "c"};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message ab c");
-    }
-
-    @Test
-    void testFormatNullArgs() {
-        final String testMsg = "Test message {} {} {} {} {} {}";
-        final String[] args = {"a", null, "c", null, null, null};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message a null c null null null");
-    }
-
-    @Test
-    void testFormatStringArgsIgnoresSuperfluousArgs() {
-        final String testMsg = "Test message {}{} {}";
-        final String[] args = {"a", "b", "c", "unnecessary", "superfluous"};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message ab c");
-    }
-
-    @Test
-    void testFormatStringArgsWithEscape() {
-        final String testMsg = "Test message \\{}{} {}";
-        final String[] args = {"a", "b", "c"};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message {}a b");
-    }
-
-    @Test
-    void testFormatStringArgsWithTrailingEscape() {
-        final String testMsg = "Test message {}{} {}\\";
-        final String[] args = {"a", "b", "c"};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message ab c\\");
-    }
-
-    @Test
-    void testFormatStringArgsWithTrailingText() {
-        final String testMsg = "Test message {}{} {}Text";
-        final String[] args = {"a", "b", "c"};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message ab cText");
-    }
-
-    @Test
-    void testFormatStringArgsWithTrailingEscapedEscape() {
-        final String testMsg = "Test message {}{} {}\\\\";
-        final String[] args = {"a", "b", "c"};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message ab c\\");
-    }
-
-    @Test
-    void testFormatStringArgsWithEscapedEscape() {
-        final String testMsg = "Test message \\\\{}{} {}";
-        final String[] args = {"a", "b", "c"};
-        final String result = ParameterizedMessage.format(testMsg, args);
-        assertThat(result).isEqualTo("Test message \\ab c");
-    }
-
-    @Test
-    void testSafeWithMutableParams() { // LOG4J2-763
-        final String testMsg = "Test message {}";
-        final Mutable param = new Mutable().set("abc");
-        final ParameterizedMessage msg = new ParameterizedMessage(testMsg, 
param);
-
-        // modify parameter before calling msg.getFormattedMessage
-        param.set("XYZ");
-        final String actual = msg.getFormattedMessage();
-        assertThat(actual).isEqualTo("Test message XYZ").as("Should use 
current param value");
-
-        // modify parameter after calling msg.getFormattedMessage
-        param.set("000");
-        final String after = msg.getFormattedMessage();
-        assertThat(after).isEqualTo("Test message XYZ").as("Should not change 
after rendered once");
-    }
-
-    static Stream<Object> testSerializable() {
-        @SuppressWarnings("EqualsHashCode")
-        class NonSerializable {
-            @Override
-            public boolean equals(final Object other) {
-                return other instanceof NonSerializable; // a very lenient 
equals()
-            }
-        }
-        return Stream.of(
-                "World",
-                new NonSerializable(),
-                new BigDecimal("123.456"),
-                // LOG4J2-3680
-                new RuntimeException(),
-                null);
-    }
-
-    @ParameterizedTest
-    @MethodSource
-    void testSerializable(final Object arg) {
-        final Message expected = new ParameterizedMessage("Hello {}!", arg);
-        final Message actual = 
SerialUtil.deserialize(SerialUtil.serialize(expected));
-        assertThat(actual).isInstanceOf(ParameterizedMessage.class);
-        
assertThat(actual.getFormattedMessage()).isEqualTo(expected.getFormattedMessage());
-    }
-
-    /**
-     * In this test cases, constructed the following scenarios: <br>
-     * <p>
-     * 1. The arguments contains an exception, and the count of placeholder is 
equal to arguments include exception. <br>
-     * 2. The arguments contains an exception, and the count of placeholder is 
equal to arguments except exception.<br>
-     * All of these should not logged in status logger.
-     * </p>
-     *
-     * @return Streams
-     */
-    static Stream<Object[]> testCasesWithExceptionArgsButNoWarn() {
-        return Stream.of(
-                new Object[] {
-                    "with exception {} {}",
-                    new Object[] {"a", new RuntimeException()},
-                    "with exception a java.lang.RuntimeException"
-                },
-                new Object[] {
-                    "with exception {} {}", new Object[] {"a", "b", new 
RuntimeException()}, "with exception a b"
-                });
-    }
-
-    @ParameterizedTest
-    @MethodSource("testCasesWithExceptionArgsButNoWarn")
-    void formatToWithExceptionButNoWarn(final String pattern, final Object[] 
args, final String expected) {
-        final ParameterizedMessage message = new ParameterizedMessage(pattern, 
args);
-        final StringBuilder buffer = new StringBuilder();
-        message.formatTo(buffer);
-        assertThat(buffer.toString()).isEqualTo(expected);
-        final List<StatusData> statusDataList = 
statusListener.getStatusData().collect(Collectors.toList());
-        assertThat(statusDataList).hasSize(0);
-    }
-
-    @ParameterizedTest
-    @MethodSource("testCasesWithExceptionArgsButNoWarn")
-    void formatWithExceptionButNoWarn(final String pattern, final Object[] 
args, final String expected) {
-        final String message = ParameterizedMessage.format(pattern, args);
-        assertThat(message).isEqualTo(expected);
-        final List<StatusData> statusDataList = 
statusListener.getStatusData().collect(Collectors.toList());
-        assertThat(statusDataList).hasSize(0);
-    }
-
-    /**
-     * In this test cases, constructed the following scenarios: <br>
-     * <p>
-     * 1. The placeholders are greater than the count of arguments. <br>
-     * 2. The placeholders are less than the count of arguments. <br>
-     * 3. The arguments contains an exception, and the placeholder is greater 
than normal arguments. <br>
-     * 4. The arguments contains an exception, and the placeholder is less 
than the arguments.<br>
-     * All of these should logged in status logger with WARN level.
-     * </p>
-     *
-     * @return streams
-     */
-    static Stream<Object[]> testCasesForInsufficientFormatArgs() {
-        return Stream.of(
-                new Object[] {"more {} {}", 2, new Object[] {"a"}, "more a 
{}"},
-                new Object[] {"more {} {} {}", 3, new Object[] {"a"}, "more a 
{} {}"},
-                new Object[] {"less {}", 1, new Object[] {"a", "b"}, "less a"},
-                new Object[] {"less {} {}", 2, new Object[] {"a", "b", "c"}, 
"less a b"},
-                new Object[] {
-                    "more throwable {} {} {}",
-                    3,
-                    new Object[] {"a", new RuntimeException()},
-                    "more throwable a java.lang.RuntimeException {}"
-                },
-                new Object[] {
-                    "less throwable {}", 1, new Object[] {"a", "b", new 
RuntimeException()}, "less throwable a"
-                });
-    }
-
-    @ParameterizedTest
-    @MethodSource("testCasesForInsufficientFormatArgs")
-    void formatToShouldWarnOnInsufficientArgs(
-            final String pattern, final int placeholderCount, final Object[] 
args, final String expected) {
-        final int argCount = args == null ? 0 : args.length;
-        verifyFormattingFailureOnInsufficientArgs(pattern, placeholderCount, 
argCount, expected, () -> {
-            final ParameterizedMessage message = new 
ParameterizedMessage(pattern, args);
-            final StringBuilder buffer = new StringBuilder();
-            message.formatTo(buffer);
-            return buffer.toString();
-        });
-    }
-
-    @ParameterizedTest
-    @MethodSource("testCasesForInsufficientFormatArgs")
-    void formatShouldWarnOnInsufficientArgs(
-            final String pattern, final int placeholderCount, final Object[] 
args, final String expected) {
-        final int argCount = args == null ? 0 : args.length;
-        verifyFormattingFailureOnInsufficientArgs(
-                pattern, placeholderCount, argCount, expected, () -> 
ParameterizedMessage.format(pattern, args));
-    }
-
-    private void verifyFormattingFailureOnInsufficientArgs(
-            final String pattern,
-            final int placeholderCount,
-            final int argCount,
-            final String expected,
-            final Supplier<String> formattedMessageSupplier) {
-
-        // Verify the formatted message
-        final String formattedMessage = formattedMessageSupplier.get();
-        assertThat(formattedMessage).isEqualTo(expected);
-
-        // Verify the status logger warn
-        final List<StatusData> statusDataList = 
statusListener.getStatusData().collect(Collectors.toList());
-        assertThat(statusDataList).hasSize(1);
-        final StatusData statusData = statusDataList.get(0);
-        assertThat(statusData.getLevel()).isEqualTo(Level.WARN);
-        assertThat(statusData.getMessage().getFormattedMessage())
-                .isEqualTo(
-                        "found %d argument placeholders, but provided %d for 
pattern `%s`",
-                        placeholderCount, argCount, pattern);
-        assertThat(statusData.getThrowable()).isNull();
-    }
-}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
new file mode 100644
index 0000000000..bac943751c
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
@@ -0,0 +1,276 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.ParameterizedMapMessageFactory;
+import org.apache.logging.log4j.spi.AbstractLogger;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.StackLocatorUtil;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * Logger for resources. Formats all events using the 
ParameterizedMapMessageFactory along with the provided
+ * Supplier. The Supplier provides resource attributes that should be included 
in all log events generated
+ * from the current resource. Note that since the Supplier is called for every 
LogEvent being generated
+ * the values returned may change as necessary. Care should be taken to make 
the Supplier as efficient as
+ * possible to avoid performance issues.
+ *
+ * Unlike regular Loggers ResourceLoggers CANNOT be declared to be static. A 
ResourceLogger
+ * must be declared as a class member that will be garbage collected along 
with the instance of the resource.
+ */
+public final class ResourceLogger extends AbstractLogger {
+    private static final long serialVersionUID = -5837924138744974513L;
+    private final ExtendedLogger logger;
+
+    public static ResourceLoggerBuilder newBuilder() {
+        return new ResourceLoggerBuilder();
+    }
+
+    /*
+     * Pass our MessageFactory with its Supplier to AbstractLogger. This will 
be used to create
+     * the Messages prior to them being passed to the "real" Logger.
+     */
+    private ResourceLogger(final ExtendedLogger logger, final 
Supplier<Map<String, String>> supplier) {
+        super(logger.getName(), new ParameterizedMapMessageFactory(supplier));
+        this.logger = logger;
+    }
+
+    @Override
+    public Level getLevel() {
+        return logger.getLevel();
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, Message message, 
Throwable t) {
+        return logger.isEnabled(level, marker, message, t);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, CharSequence message, 
Throwable t) {
+        return logger.isEnabled(level, marker, message, t);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, Object message, 
Throwable t) {
+        return logger.isEnabled(level, marker, message, t);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, String message, 
Throwable t) {
+        return logger.isEnabled(level, marker, message, t);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, String message) {
+        return logger.isEnabled(level, marker, message);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, String message, 
Object... params) {
+        return logger.isEnabled(level, marker, message, params);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, String message, 
Object p0) {
+        return logger.isEnabled(level, marker, message, p0);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, String message, 
Object p0, Object p1) {
+        return logger.isEnabled(level, marker, message, p0, p1);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, String message, 
Object p0, Object p1, Object p2) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2);
+    }
+
+    @Override
+    public boolean isEnabled(Level level, Marker marker, String message, 
Object p0, Object p1, Object p2, Object p3) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2, p3);
+    }
+
+    @Override
+    public boolean isEnabled(
+            Level level, Marker marker, String message, Object p0, Object p1, 
Object p2, Object p3, Object p4) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4);
+    }
+
+    @Override
+    public boolean isEnabled(
+            Level level,
+            Marker marker,
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4, 
p5);
+    }
+
+    @Override
+    public boolean isEnabled(
+            Level level,
+            Marker marker,
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4, 
p5, p6);
+    }
+
+    @Override
+    public boolean isEnabled(
+            Level level,
+            Marker marker,
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6,
+            Object p7) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4, 
p5, p6, p7);
+    }
+
+    @Override
+    public boolean isEnabled(
+            Level level,
+            Marker marker,
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6,
+            Object p7,
+            Object p8) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4, 
p5, p6, p7, p8);
+    }
+
+    @Override
+    public boolean isEnabled(
+            Level level,
+            Marker marker,
+            String message,
+            Object p0,
+            Object p1,
+            Object p2,
+            Object p3,
+            Object p4,
+            Object p5,
+            Object p6,
+            Object p7,
+            Object p8,
+            Object p9) {
+        return logger.isEnabled(level, marker, message, p0, p1, p2, p3, p4, 
p5, p6, p7, p8, p9);
+    }
+
+    @Override
+    public void logMessage(String fqcn, Level level, Marker marker, Message 
message, Throwable t) {
+        logger.logMessage(fqcn, level, marker, message, t);
+    }
+
+    /**
+     * Constructs a ResourceLogger.
+     */
+    public static final class ResourceLoggerBuilder {
+        private static final Logger LOGGER = StatusLogger.getLogger();
+        private ExtendedLogger logger;
+        private String name;
+        private Supplier<Map<String, String>> supplier;
+
+        /**
+         * Create the builder.
+         */
+        private ResourceLoggerBuilder() {}
+
+        /**
+         * Add the underlying Logger to use. If a Logger, logger name, or 
class is not required
+         * the name of the calling class wiill be used.
+         * @param logger The Logger to use.
+         * @return The ResourceLoggerBuilder.
+         */
+        public ResourceLoggerBuilder withLogger(ExtendedLogger logger) {
+            this.logger = logger;
+            return this;
+        }
+
+        /**
+         * Add the Logger name. If a Logger, logger name, or class is not 
required
+         * the name of the calling class wiill be used.
+         * @param name the name to assign to the Logger.
+         * @return The ResourceLoggerBuilder.
+         */
+        public ResourceLoggerBuilder withName(String name) {
+            this.name = name;
+            return this;
+        }
+
+        /**
+         * The resource Class. If a Logger, logger name, or class is not 
required
+         * the name of the calling class wiill be used.
+         * @param clazz the resource Class.
+         * @return the ResourceLoggerBuilder.
+         */
+        public ResourceLoggerBuilder withClass(Class<?> clazz) {
+            this.name = clazz.getCanonicalName() != null ? 
clazz.getCanonicalName() : clazz.getName();
+            return this;
+        }
+
+        /**
+         * The Map Supplier.
+         * @param supplier the method that provides the Map of resource data 
to include in logs.
+         * @return the ResourceLoggerBuilder.
+         */
+        public ResourceLoggerBuilder withSupplier(Supplier<Map<String, 
String>> supplier) {
+            this.supplier = supplier;
+            return this;
+        }
+
+        /**
+         * Construct the ResourceLogger.
+         * @return the ResourceLogger.
+         */
+        public ResourceLogger build() {
+            if (this.logger == null) {
+                if (Strings.isEmpty(name)) {
+                    Class<?> clazz = StackLocatorUtil.getCallerClass(2);
+                    name = clazz.getCanonicalName() != null ? 
clazz.getCanonicalName() : clazz.getName();
+                }
+                this.logger = (ExtendedLogger) LogManager.getLogger(name);
+            }
+            Supplier<Map<String, String>> mapSupplier = this.supplier != null 
? this.supplier : Collections::emptyMap;
+            return new ResourceLogger(logger, mapSupplier);
+        }
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
new file mode 100644
index 0000000000..0806f820c5
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
@@ -0,0 +1,558 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.internal.ScopedContextAnchor;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * Context that can be used for data to be logged in a block of code.
+ *
+ * While this is influenced by ScopedValues from Java 21 it does not share the 
same API. While it can perform a
+ * similar function as a set of ScopedValues it is really meant to allow a 
block of code to include a set of keys and
+ * values in all the log events within that block. The underlying 
implementation must provide support for
+ * logging the ScopedContext for that to happen.
+ *
+ * The ScopedContext will not be bound to the current thread until either a 
run or call method is invoked. The
+ * contexts are nested so creating and running or calling via a second 
ScopedContext will result in the first
+ * ScopedContext being hidden until the call is returned. Thus the values from 
the first ScopedContext need to
+ * be added to the second to be included.
+ *
+ * The ScopedContext can be passed to child threads by including the 
ExecutorService to be used to manage the
+ * run or call methods. The caller should interact with the ExecutorService as 
if they were submitting their
+ * run or call methods directly to it. The ScopedContext performs no error 
handling other than to ensure the
+ * ThreadContext and ScopedContext are cleaned up from the executed Thread.
+ *
+ * @since 2.24.0
+ */
+public class ScopedContext {
+
+    public static final Logger LOGGER = StatusLogger.getLogger();
+
+    /**
+     * @hidden
+     * Returns an unmodifiable copy of the current ScopedContext Map. This 
method should
+     * only be used by implementations of Log4j API.
+     * @return the Map of Renderable objects.
+     */
+    public static Map<String, Renderable> getContextMap() {
+        Optional<Instance> context = ScopedContextAnchor.getContext();
+        if (context.isPresent()
+                && context.get().contextMap != null
+                && !context.get().contextMap.isEmpty()) {
+            return Collections.unmodifiableMap(context.get().contextMap);
+        }
+        return Collections.emptyMap();
+    }
+
+    /**
+     * Return the key from the current ScopedContext, if there is one and the 
key exists.
+     * @param key The key.
+     * @return The value of the key in the current ScopedContext.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T get(String key) {
+        Optional<Instance> context = ScopedContextAnchor.getContext();
+        if (context.isPresent()) {
+            Renderable renderable = context.get().contextMap.get(key);
+            if (renderable != null) {
+                return (T) renderable.getObject();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Creates a ScopedContext Instance with a key/value pair.
+     *
+     * @param key   the key to add.
+     * @param value the value associated with the key.
+     * @return the Instance constructed if a valid key and value were 
provided. Otherwise, either the
+     * current Instance is returned or a new Instance is created if there is 
no current Instance.
+     */
+    public static Instance where(String key, Object value) {
+        if (value != null) {
+            Renderable renderable = value instanceof Renderable ? (Renderable) 
value : new ObjectRenderable(value);
+            Instance parent = current().isPresent() ? current().get() : null;
+            return new Instance(parent, key, renderable);
+        } else {
+            if (current().isPresent()) {
+                Map<String, Renderable> map = getContextMap();
+                map.remove(key);
+                return new Instance(map);
+            }
+        }
+        return current().isPresent() ? current().get() : new Instance();
+    }
+
+    /**
+     * Adds a key/value pair to the ScopedContext being constructed.
+     *
+     * @param key      the key to add.
+     * @param supplier the function to generate the value.
+     * @return the ScopedContext being constructed.
+     */
+    public static Instance where(String key, Supplier<Object> supplier) {
+        return where(key, supplier.get());
+    }
+
+    /**
+     * Creates a ScopedContext Instance with a Map of keys and values.
+     * @param map the Map.
+     * @return the ScopedContext Instance constructed.
+     */
+    public static Instance where(Map<String, Object> map) {
+        if (map != null && !map.isEmpty()) {
+            Map<String, Renderable> renderableMap = new HashMap<>();
+            if (current().isPresent()) {
+                renderableMap.putAll(current().get().contextMap);
+            }
+            map.forEach((key, value) -> {
+                if (value == null || (value instanceof String && ((String) 
value).isEmpty())) {
+                    renderableMap.remove(key);
+                } else {
+                    renderableMap.put(
+                            key, value instanceof Renderable ? (Renderable) 
value : new ObjectRenderable(value));
+                }
+            });
+            return new Instance(renderableMap);
+        } else {
+            return current().isPresent() ? current().get() : new Instance();
+        }
+    }
+
+    /**
+     * Creates a ScopedContext with a single key/value pair and calls a method.
+     * @param key the key.
+     * @param obj the value associated with the key.
+     * @param op the Runnable to call.
+     */
+    public static void runWhere(String key, Object obj, Runnable op) {
+        if (obj != null) {
+            Renderable renderable = obj instanceof Renderable ? (Renderable) 
obj : new ObjectRenderable(obj);
+            Map<String, Renderable> map = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.put(key, renderable);
+            new Instance(map).run(op);
+        } else {
+            Map<String, Renderable> map = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.remove(key);
+            new Instance(map).run(op);
+        }
+    }
+
+    /**
+     * Creates a ScopedContext with a single key/value pair and calls a method 
on a separate Thread.
+     * @param key the key.
+     * @param obj the value associated with the key.
+     * @param executorService the ExecutorService to dispatch the work.
+     * @param op the Runnable to call.
+     */
+    public static Future<?> runWhere(String key, Object obj, ExecutorService 
executorService, Runnable op) {
+        if (obj != null) {
+            Renderable renderable = obj instanceof Renderable ? (Renderable) 
obj : new ObjectRenderable(obj);
+            Map<String, Renderable> map = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.put(key, renderable);
+            if (executorService != null) {
+                return executorService.submit(new Runner(
+                        new Instance(map), ThreadContext.getContext(), 
ThreadContext.getImmutableStack(), op));
+            } else {
+                new Instance(map).run(op);
+                return CompletableFuture.completedFuture(0);
+            }
+        } else {
+            Map<String, Renderable> map = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.remove(key);
+            if (executorService != null) {
+                return executorService.submit(new Runner(
+                        new Instance(map), ThreadContext.getContext(), 
ThreadContext.getImmutableStack(), op));
+            } else {
+                new Instance(map).run(op);
+                return CompletableFuture.completedFuture(0);
+            }
+        }
+    }
+
+    /**
+     * Creates a ScopedContext with a Map of keys and values and calls a 
method.
+     * @param map the Map.
+     * @param op the Runnable to call.
+     */
+    public static void runWhere(Map<String, Object> map, Runnable op) {
+        if (map != null && !map.isEmpty()) {
+            Map<String, Renderable> renderableMap = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.forEach((key, value) -> {
+                renderableMap.put(key, value instanceof Renderable ? 
(Renderable) value : new ObjectRenderable(value));
+            });
+            new Instance(renderableMap).run(op);
+        } else {
+            op.run();
+        }
+    }
+
+    /**
+     * Creates a ScopedContext with a single key/value pair and calls a method.
+     * @param key the key.
+     * @param obj the value associated with the key.
+     * @param op the Runnable to call.
+     */
+    public static <R> R callWhere(String key, Object obj, Callable<R> op) 
throws Exception {
+        if (obj != null) {
+            Renderable renderable = obj instanceof Renderable ? (Renderable) 
obj : new ObjectRenderable(obj);
+            Map<String, Renderable> map = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.put(key, renderable);
+            return new Instance(map).call(op);
+        } else {
+            Map<String, Renderable> map = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.remove(key);
+            return new Instance(map).call(op);
+        }
+    }
+
+    /**
+     * Creates a ScopedContext with a single key/value pair and calls a method 
on a separate Thread.
+     * @param key the key.
+     * @param obj the value associated with the key.
+     * @param executorService the ExecutorService to dispatch the work.
+     * @param op the Callable to call.
+     */
+    public static <R> Future<R> callWhere(String key, Object obj, 
ExecutorService executorService, Callable<R> op)
+            throws Exception {
+        if (obj != null) {
+            Renderable renderable = obj instanceof Renderable ? (Renderable) 
obj : new ObjectRenderable(obj);
+            Map<String, Renderable> map = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.put(key, renderable);
+            if (executorService != null) {
+                return executorService.submit(new Caller<R>(
+                        new Instance(map), ThreadContext.getContext(), 
ThreadContext.getImmutableStack(), op));
+            } else {
+                R ret = new Instance(map).call(op);
+                return CompletableFuture.completedFuture(ret);
+            }
+        } else {
+            if (executorService != null) {
+                Map<String, Renderable> map = new HashMap<>();
+                if (current().isPresent()) {
+                    map.putAll(current().get().contextMap);
+                }
+                map.remove(key);
+                return executorService.submit(new Caller<R>(
+                        new Instance(map), ThreadContext.getContext(), 
ThreadContext.getImmutableStack(), op));
+            } else {
+                R ret = op.call();
+                return CompletableFuture.completedFuture(ret);
+            }
+        }
+    }
+
+    /**
+     * Creates a ScopedContext with a Map of keys and values and calls a 
method.
+     * @param map the Map.
+     * @param op the Runnable to call.
+     */
+    public static <R> R callWhere(Map<String, Object> map, Callable<R> op) 
throws Exception {
+        if (map != null && !map.isEmpty()) {
+            Map<String, Renderable> renderableMap = new HashMap<>();
+            if (current().isPresent()) {
+                map.putAll(current().get().contextMap);
+            }
+            map.forEach((key, value) -> {
+                renderableMap.put(key, value instanceof Renderable ? 
(Renderable) value : new ObjectRenderable(value));
+            });
+            return new Instance(renderableMap).call(op);
+        } else {
+            return op.call();
+        }
+    }
+
+    /**
+     * Returns an Optional holding the active ScopedContext.Instance
+     * @return an Optional containing the active ScopedContext, if there is 
one.
+     */
+    private static Optional<Instance> current() {
+        return ScopedContextAnchor.getContext();
+    }
+
+    public static class Instance {
+
+        private final Instance parent;
+        private final String key;
+        private final Renderable value;
+        private final Map<String, Renderable> contextMap;
+
+        private Instance() {
+            this.parent = null;
+            this.key = null;
+            this.value = null;
+            this.contextMap = null;
+        }
+
+        private Instance(Map<String, Renderable> map) {
+            this.parent = null;
+            this.key = null;
+            this.value = null;
+            this.contextMap = map;
+        }
+
+        private Instance(Instance parent, String key, Renderable value) {
+            this.parent = parent;
+            this.key = key;
+            this.value = value;
+            this.contextMap = null;
+        }
+
+        /**
+         * Adds a key/value pair to the ScopedContext being constructed.
+         *
+         * @param key   the key to add.
+         * @param value the value associated with the key.
+         * @return the ScopedContext being constructed.
+         */
+        public Instance where(String key, Object value) {
+            return addObject(key, value);
+        }
+
+        /**
+         * Adds a key/value pair to the ScopedContext being constructed.
+         *
+         * @param key      the key to add.
+         * @param supplier the function to generate the value.
+         * @return the ScopedContext being constructed.
+         */
+        public Instance where(String key, Supplier<Object> supplier) {
+            return addObject(key, supplier.get());
+        }
+
+        private Instance addObject(String key, Object obj) {
+            if (obj != null) {
+                Renderable renderable = obj instanceof Renderable ? 
(Renderable) obj : new ObjectRenderable(obj);
+                return new Instance(this, key, renderable);
+            }
+            return this;
+        }
+
+        /**
+         * Executes a code block that includes all the key/value pairs added 
to the ScopedContext.
+         *
+         * @param op the code block to execute.
+         */
+        public void run(Runnable op) {
+            new Runner(this, null, null, op).run();
+        }
+
+        /**
+         * Executes a code block that includes all the key/value pairs added 
to the ScopedContext on a different Thread.
+         *
+         * @param op the code block to execute.
+         * @return a Future representing pending completion of the task
+         */
+        public Future<?> run(ExecutorService executorService, Runnable op) {
+            return executorService.submit(
+                    new Runner(this, ThreadContext.getContext(), 
ThreadContext.getImmutableStack(), op));
+        }
+
+        /**
+         * Executes a code block that includes all the key/value pairs added 
to the ScopedContext.
+         *
+         * @param op the code block to execute.
+         * @return the return value from the code block.
+         */
+        public <R> R call(Callable<R> op) throws Exception {
+            return new Caller<R>(this, null, null, op).call();
+        }
+
+        /**
+         * Executes a code block that includes all the key/value pairs added 
to the ScopedContext on a different Thread.
+         *
+         * @param op the code block to execute.
+         * @return a Future representing pending completion of the task
+         */
+        public <R> Future<R> call(ExecutorService executorService, Callable<R> 
op) {
+            return executorService.submit(
+                    new Caller<R>(this, ThreadContext.getContext(), 
ThreadContext.getImmutableStack(), op));
+        }
+    }
+
+    private static class Runner implements Runnable {
+        private final Map<String, Renderable> contextMap = new HashMap<>();
+        private final Map<String, String> threadContextMap;
+        private final ThreadContext.ContextStack contextStack;
+        private final Instance context;
+        private final Runnable op;
+
+        public Runner(
+                Instance context,
+                Map<String, String> threadContextMap,
+                ThreadContext.ContextStack contextStack,
+                Runnable op) {
+            this.context = context;
+            this.threadContextMap = threadContextMap;
+            this.contextStack = contextStack;
+            this.op = op;
+        }
+
+        @Override
+        public void run() {
+            Instance scopedContext = context;
+            // If the current context has a Map then we can just use it.
+            if (context.contextMap == null) {
+                do {
+                    if (scopedContext.contextMap != null) {
+                        // Once we hit a scope with an already populated Map 
we won't need to go any further.
+                        contextMap.putAll(scopedContext.contextMap);
+                        break;
+                    } else if (scopedContext.key != null) {
+                        contextMap.putIfAbsent(scopedContext.key, 
scopedContext.value);
+                    }
+                    scopedContext = scopedContext.parent;
+                } while (scopedContext != null);
+                scopedContext = new Instance(contextMap);
+            }
+            if (threadContextMap != null && !threadContextMap.isEmpty()) {
+                ThreadContext.putAll(threadContextMap);
+            }
+            if (contextStack != null) {
+                ThreadContext.setStack(contextStack);
+            }
+            ScopedContextAnchor.addScopedContext(scopedContext);
+            try {
+                op.run();
+            } finally {
+                ScopedContextAnchor.removeScopedContext();
+                ThreadContext.clearAll();
+            }
+        }
+    }
+
+    private static class Caller<R> implements Callable<R> {
+        private final Map<String, Renderable> contextMap = new HashMap<>();
+        private final Instance context;
+        private final Map<String, String> threadContextMap;
+        private final ThreadContext.ContextStack contextStack;
+        private final Callable<R> op;
+
+        public Caller(
+                Instance context,
+                Map<String, String> threadContextMap,
+                ThreadContext.ContextStack contextStack,
+                Callable<R> op) {
+            this.context = context;
+            this.threadContextMap = threadContextMap;
+            this.contextStack = contextStack;
+            this.op = op;
+        }
+
+        @Override
+        public R call() throws Exception {
+            Instance scopedContext = context;
+            // If the current context has a Map then we can just use it.
+            if (context.contextMap == null) {
+                do {
+                    if (scopedContext.contextMap != null) {
+                        // Once we hit a scope with an already populated Map 
we won't need to go any further.
+                        contextMap.putAll(scopedContext.contextMap);
+                        break;
+                    } else if (scopedContext.key != null) {
+                        contextMap.putIfAbsent(scopedContext.key, 
scopedContext.value);
+                    }
+                    scopedContext = scopedContext.parent;
+                } while (scopedContext != null);
+                scopedContext = new Instance(contextMap);
+            }
+            if (threadContextMap != null && !threadContextMap.isEmpty()) {
+                ThreadContext.putAll(threadContextMap);
+            }
+            if (contextStack != null) {
+                ThreadContext.setStack(contextStack);
+            }
+            ScopedContextAnchor.addScopedContext(scopedContext);
+            try {
+                return op.call();
+            } finally {
+                ScopedContextAnchor.removeScopedContext();
+                ThreadContext.clearAll();
+            }
+        }
+    }
+
+    /**
+     * Interface for converting Objects stored in the ContextScope to Strings 
for logging.
+     */
+    public static interface Renderable {
+        /**
+         * Render the object as a String.
+         * @return the String representation of the Object.
+         */
+        default String render() {
+            return this.toString();
+        }
+
+        default Object getObject() {
+            return this;
+        }
+    }
+
+    private static class ObjectRenderable implements Renderable {
+        private final Object object;
+
+        public ObjectRenderable(Object object) {
+            this.object = object;
+        }
+
+        @Override
+        public String render() {
+            return object.toString();
+        }
+
+        @Override
+        public Object getObject() {
+            return object;
+        }
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
new file mode 100644
index 0000000000..c09c4bc78f
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
@@ -0,0 +1,69 @@
+/*
+ * 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.internal;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Optional;
+import org.apache.logging.log4j.ScopedContext;
+
+/**
+ * Anchor for the ScopedContext. This class is private and not for public 
consumption.
+ */
+public class ScopedContextAnchor {
+    private static final ThreadLocal<Deque<ScopedContext.Instance>> 
scopedContext = new ThreadLocal<>();
+
+    /**
+     * Returns an immutable Map containing all the key/value pairs as 
Renderable objects.
+     * @return An immutable copy of the Map at the current scope.
+     */
+    public static Optional<ScopedContext.Instance> getContext() {
+        Deque<ScopedContext.Instance> stack = scopedContext.get();
+        if (stack != null) {
+            return Optional.of(stack.getFirst());
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Add the ScopeContext.
+     * @param context The ScopeContext.
+     */
+    public static void addScopedContext(ScopedContext.Instance context) {
+        Deque<ScopedContext.Instance> stack = scopedContext.get();
+        if (stack == null) {
+            stack = new ArrayDeque<>();
+            scopedContext.set(stack);
+        }
+        stack.addFirst(context);
+    }
+
+    /**
+     * Remove the top ScopeContext.
+     */
+    public static void removeScopedContext() {
+        Deque<ScopedContext.Instance> stack = scopedContext.get();
+        if (stack != null) {
+            if (!stack.isEmpty()) {
+                stack.removeFirst();
+            }
+            if (stack.isEmpty()) {
+                scopedContext.remove();
+            }
+        }
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessage.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessage.java
new file mode 100644
index 0000000000..292bbb8290
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessage.java
@@ -0,0 +1,38 @@
+/*
+ * 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.message;
+
+import java.util.Map;
+
+/**
+ * Class Description goes here.
+ */
+public class ParameterizedMapMessage extends StringMapMessage {
+
+    private static final long serialVersionUID = -7724723101786525409L;
+    private final Message baseMessage;
+
+    ParameterizedMapMessage(Message baseMessage, Map<String, String> 
resourceMap) {
+        super(resourceMap);
+        this.baseMessage = baseMessage;
+    }
+
+    @Override
+    public String getFormattedMessage() {
+        return baseMessage.getFormattedMessage();
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessageFactory.java
 
b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessageFactory.java
new file mode 100644
index 0000000000..48575c0849
--- /dev/null
+++ 
b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessageFactory.java
@@ -0,0 +1,216 @@
+/*
+ * 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.message;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Extends a StringMapMessage to appender a "normal" Parameterized message to 
the Map data.
+ */
+public class ParameterizedMapMessageFactory extends AbstractMessageFactory {
+
+    private final Supplier<Map<String, String>> mapSupplier;
+
+    public ParameterizedMapMessageFactory(Supplier<Map<String, String>> 
mapSupplier) {
+        this.mapSupplier = mapSupplier;
+    }
+
+    @Override
+    public Message newMessage(final CharSequence message) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new SimpleMessage(message);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    @Override
+    public Message newMessage(final Object message) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ObjectMessage(message);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    @Override
+    public Message newMessage(final String message) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new SimpleMessage(message);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object... params) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, params);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object p0) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object p0, final 
Object p1) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    @Override
+    public Message newMessage(final String message, final Object p0, final 
Object p1, final Object p2) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final 
Object p2, final Object p3) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2, p3);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message, final Object p0, final Object p1, final 
Object p2, final Object p3, final Object p4) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2, p3, p4);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2, p3, p4, 
p5);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2, p3, p4, 
p5, p6);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2, p3, p4, 
p5, p6, p7);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2, p3, p4, 
p5, p6, p7, p8);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    /**
+     * @since 2.6.1
+     */
+    @Override
+    public Message newMessage(
+            final String message,
+            final Object p0,
+            final Object p1,
+            final Object p2,
+            final Object p3,
+            final Object p4,
+            final Object p5,
+            final Object p6,
+            final Object p7,
+            final Object p8,
+            final Object p9) {
+        Map<String, String> map = mapSupplier.get();
+        Message msg = new ParameterizedMessage(message, p0, p1, p2, p3, p4, 
p5, p6, p7, p8, p9);
+        return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof ParameterizedMapMessageFactory)) {
+            return false;
+        }
+        ParameterizedMapMessageFactory that = (ParameterizedMapMessageFactory) 
o;
+        return Objects.equals(mapSupplier, that.mapSupplier);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mapSupplier);
+    }
+}
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/message/package-info.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/message/package-info.java
index 24632f8d7b..393e7b517f 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/package-info.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/package-info.java
@@ -20,7 +20,7 @@
  */
 @Export
 /**
- * Bumped to 2.22.0, since FormattedMessage behavior changed.
+ * Bumped to 2.24.0, to add ParameterizedMapMessage.
  */
 @Version("2.24.0")
 package org.apache.logging.log4j.message;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java
index 5407f05f61..f1c67c6c86 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/package-info.java
@@ -32,7 +32,7 @@
  * @see <a href="http://logging.apache.org/log4j/2.x/manual/api.html";>Log4j 2 
API manual</a>
  */
 @Export
-@Version("2.20.2")
+@Version("2.24.0")
 package org.apache.logging.log4j;
 
 import org.osgi.annotation.bundle.Export;
diff --git 
a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java 
b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
index 1690893187..f5529f4258 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
@@ -21,9 +21,11 @@ import java.io.PrintStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.Map;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.ScopedContext;
 import org.apache.logging.log4j.ThreadContext;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
@@ -294,8 +296,9 @@ public class SimpleLogger extends AbstractLogger {
         }
         sb.append(msg.getFormattedMessage());
         if (showContextMap) {
-            final Map<String, String> mdc = 
ThreadContext.getImmutableContext();
-            if (mdc.size() > 0) {
+            final Map<String, String> mdc = new 
HashMap<>(ThreadContext.getImmutableContext());
+            ScopedContext.getContextMap().forEach((key, value) -> mdc.put(key, 
value.render()));
+            if (!mdc.isEmpty()) {
                 sb.append(SPACE);
                 sb.append(mdc.toString());
                 sb.append(SPACE);
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
new file mode 100644
index 0000000000..b62784b4c7
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.message;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.aMapWithSize;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.ResourceLogger;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
+import org.apache.logging.log4j.core.test.junit.Named;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the ParameterizedMapMessageFactory class.
+ */
+@LoggerContextSource("log4j-map.xml")
+public class ResourceLoggerTest {
+
+    private final ListAppender app;
+
+    public ResourceLoggerTest(@Named("List") final ListAppender list) {
+        app = list.clear();
+    }
+
+    @Test
+    public void testFactory(final LoggerContext context) throws Exception {
+        Connection connection = new Connection("Test", "dummy");
+        connection.useConnection();
+        MapSupplier mapSupplier = new MapSupplier(connection);
+        ResourceLogger logger = ResourceLogger.newBuilder()
+                .withClass(this.getClass())
+                .withSupplier(mapSupplier)
+                .build();
+        logger.debug("Hello, {}", "World");
+        List<LogEvent> events = app.getEvents();
+        assertThat(events, hasSize(1));
+        Message message = events.get(0).getMessage();
+        assertTrue(message instanceof ParameterizedMapMessage);
+        Map<String, String> data = ((ParameterizedMapMessage) 
message).getData();
+        assertThat(data, aMapWithSize(3));
+        assertEquals("Test", data.get("Name"));
+        assertEquals("dummy", data.get("Type"));
+        assertEquals("1", data.get("Count"));
+        assertEquals("Hello, World", message.getFormattedMessage());
+        assertEquals(this.getClass().getName(), events.get(0).getLoggerName());
+        assertEquals(this.getClass().getName(), 
events.get(0).getSource().getClassName());
+        app.clear();
+        connection.useConnection();
+        logger.debug("Used the connection");
+        events = app.getEvents();
+        assertThat(events, hasSize(1));
+        message = events.get(0).getMessage();
+        assertTrue(message instanceof ParameterizedMapMessage);
+        data = ((ParameterizedMapMessage) message).getData();
+        assertThat(data, aMapWithSize(3));
+        assertEquals("2", data.get("Count"));
+        app.clear();
+        connection = new Connection("NewConnection", "fiber");
+        connection.useConnection();
+        mapSupplier = new MapSupplier(connection);
+        logger = ResourceLogger.newBuilder().withSupplier(mapSupplier).build();
+        logger.debug("Connection: {}", "NewConnection");
+        events = app.getEvents();
+        assertThat(events, hasSize(1));
+        message = events.get(0).getMessage();
+        assertTrue(message instanceof ParameterizedMapMessage);
+        data = ((ParameterizedMapMessage) message).getData();
+        assertThat(data, aMapWithSize(3));
+        assertEquals("NewConnection", data.get("Name"));
+        assertEquals("fiber", data.get("Type"));
+        assertEquals("1", data.get("Count"));
+        assertEquals("Connection: NewConnection", 
message.getFormattedMessage());
+        assertEquals(this.getClass().getName(), events.get(0).getLoggerName());
+        assertEquals(this.getClass().getName(), 
events.get(0).getSource().getClassName());
+        app.clear();
+    }
+
+    private static class MapSupplier implements Supplier<Map<String, String>> {
+
+        private final Connection connection;
+
+        public MapSupplier(final Connection connection) {
+            this.connection = connection;
+        }
+
+        @Override
+        public Map<String, String> get() {
+            Map<String, String> map = new HashMap<>();
+            map.put("Name", connection.name);
+            map.put("Type", connection.type);
+            map.put("Count", Long.toString(connection.getCounter()));
+            return map;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return o instanceof MapSupplier;
+        }
+
+        @Override
+        public int hashCode() {
+            return 77;
+        }
+    }
+
+    private static class Connection {
+
+        private final String name;
+        private final String type;
+        private final AtomicLong counter = new AtomicLong(0);
+
+        public Connection(final String name, final String type) {
+            this.name = name;
+            this.type = type;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public long getCounter() {
+            return counter.get();
+        }
+
+        public void useConnection() {
+            counter.incrementAndGet();
+        }
+    }
+}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ScopedContextTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ScopedContextTest.java
new file mode 100644
index 0000000000..9953808157
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ScopedContextTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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 static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.List;
+import org.apache.logging.log4j.ScopedContext;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
+import org.apache.logging.log4j.core.test.junit.Named;
+import org.junit.jupiter.api.Test;
+
+@LoggerContextSource("log4j-list2.xml")
+public class ScopedContextTest {
+
+    private final ListAppender app;
+
+    public ScopedContextTest(@Named("List") final ListAppender list) {
+        app = list.clear();
+    }
+
+    @Test
+    public void testScope(final LoggerContext context) throws Exception {
+        final org.apache.logging.log4j.Logger logger = 
context.getLogger("org.apache.logging.log4j.scoped");
+        ScopedContext.where("key1", "Log4j2").run(() -> logger.debug("Hello, 
{}", "World"));
+        List<String> msgs = app.getMessages();
+        assertThat(msgs, hasSize(1));
+        String expected = "{key1=Log4j2}";
+        assertThat(msgs.get(0), containsString(expected));
+        app.clear();
+        ScopedContext.runWhere("key1", "value1", () -> {
+            logger.debug("Log message 1 will include key1");
+            ScopedContext.runWhere("key2", "value2", () -> logger.debug("Log 
message 2 will include key1 and key2"));
+            int count = 0;
+            try {
+                count = ScopedContext.callWhere("key2", "value2", () -> {
+                    logger.debug("Log message 2 will include key2");
+                    return 3;
+                });
+            } catch (Exception e) {
+                fail("Caught Exception: " + e.getMessage());
+            }
+            assertThat(count, equalTo(3));
+        });
+        msgs = app.getMessages();
+        assertThat(msgs, hasSize(3));
+        expected = "{key1=value1}";
+        assertThat(msgs.get(0), containsString(expected));
+        expected = "{key1=value1, key2=value2}";
+        assertThat(msgs.get(1), containsString(expected));
+        assertThat(msgs.get(2), containsString(expected));
+    }
+}
diff --git a/log4j-core-test/src/test/resources/log4j-list2.xml 
b/log4j-core-test/src/test/resources/log4j-list2.xml
new file mode 100644
index 0000000000..c747458fbd
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-list2.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<Configuration status="OFF" name="XMLConfigTest" monitorInterval="5" 
shutdownHook="disable">
+  <Appenders>
+    <List name="List">
+      <PatternLayout pattern="%d %p %C{1.} [%t] %X - %m%n"/>
+    </List>
+  </Appenders>
+
+  <Loggers>
+    <Root level="trace">
+      <AppenderRef ref="List"/>
+    </Root>
+  </Loggers>
+
+</Configuration>
diff --git a/log4j-core-test/src/test/resources/log4j-map.xml 
b/log4j-core-test/src/test/resources/log4j-map.xml
new file mode 100644
index 0000000000..1167c7cc6e
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-map.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<Configuration status="OFF" name="XMLConfigTest" monitorInterval="5" 
shutdownHook="disable">
+  <Appenders>
+    <List name="List">
+    </List>
+    <List name="List2">
+      <PatternLayout pattern="%d %p %C{1.} [%t] %X - %m%n"/>
+    </List>
+  </Appenders>
+
+  <Loggers>
+    <Root level="trace">
+      <AppenderRef ref="List"/>
+      <AppenderRef ref="List2" level="ERROR"/>
+    </Root>
+  </Loggers>
+
+</Configuration>
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
new file mode 100644
index 0000000000..a4f651b7ea
--- /dev/null
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.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.ScopedContext;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
+import org.apache.logging.log4j.util.StringMap;
+
+/**
+ * ContextDataProvider for {@code Map<String, String>} data.
+ */
+@ServiceProvider(value = ContextDataProvider.class, resolution = 
Resolution.OPTIONAL)
+public class ScopedContextDataProvider implements ContextDataProvider {
+
+    @Override
+    public Map<String, String> supplyContextData() {
+        Map<String, ScopedContext.Renderable> contextMap = 
ScopedContext.getContextMap();
+        if (!contextMap.isEmpty()) {
+            Map<String, String> map = new HashMap<>();
+            contextMap.forEach((key, value) -> map.put(key, value.render()));
+            return map;
+        } else {
+            return Collections.emptyMap();
+        }
+    }
+
+    @Override
+    public StringMap supplyStringMap() {
+        return new JdkMapAdapterStringMap(supplyContextData());
+    }
+}
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
similarity index 92%
copy from 
log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java
copy to 
log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
index c50504a872..9d76948fca 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
@@ -18,8 +18,8 @@
  * Log4j 2 private implementation classes.
  */
 @Export
-@Version("2.23.0")
-package org.apache.logging.log4j.core.impl;
+@Version("2.24.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/impl/package-info.java 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java
index c50504a872..0c3b08f43a 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/package-info.java
@@ -18,7 +18,7 @@
  * Log4j 2 private implementation classes.
  */
 @Export
-@Version("2.23.0")
+@Version("2.24.0")
 package org.apache.logging.log4j.core.impl;
 
 import org.osgi.annotation.bundle.Export;
diff --git a/src/changelog/.2.x.x/add_scoped_context.xml 
b/src/changelog/.2.x.x/add_scoped_context.xml
new file mode 100644
index 0000000000..06db3eb0d5
--- /dev/null
+++ b/src/changelog/.2.x.x/add_scoped_context.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xmlns="http://logging.apache.org/log4j/changelog";
+       xsi:schemaLocation="http://logging.apache.org/log4j/changelog 
https://logging.apache.org/log4j/changelog-0.1.3.xsd";
+       type="updated">
+  <issue id="kotlin-71" 
link="https://github.com/apache/logging-log4j-kotlin/issues/71"/>
+  <issue id="2214" 
link="https://github.com/apache/logging-log4j2/discussions/2214"/>
+  <description format="asciidoc">Add ScopedContext to log4j-api and 
ScopedContextDataProvider in log4j-core.</description>
+</entry>
diff --git a/src/site/_release-notes/_2.x.x.adoc 
b/src/site/_release-notes/_2.x.x.adoc
index 9850a3485a..fcb6f0773f 100644
--- a/src/site/_release-notes/_2.x.x.adoc
+++ b/src/site/_release-notes/_2.x.x.adoc
@@ -46,6 +46,7 @@ This releases contains ...
 [#release-notes-2-x-x-updated]
 === Updated
 
+* Add ScopedContext to log4j-api and ScopedContextDataProvider in log4j-core. 
(https://github.com/apache/logging-log4j-kotlin/issues/71[kotlin-71], 
https://github.com/apache/logging-log4j2/discussions/2214[2214])
 * Update `actions/checkout` to version `4.1.2` 
(https://github.com/apache/logging-log4j2/pull/2370[2370])
 * Update `co.elastic.clients:elasticsearch-java` to version `8.13.1` 
(https://github.com/apache/logging-log4j2/pull/2437[2437])
 * Update `com.fasterxml.jackson:jackson-bom` to version `2.17.0` 
(https://github.com/apache/logging-log4j2/pull/2372[2372])
diff --git a/src/site/asciidoc/docs.adoc b/src/site/asciidoc/docs.adoc
index effdc6e128..3bc8c0d092 100644
--- a/src/site/asciidoc/docs.adoc
+++ b/src/site/asciidoc/docs.adoc
@@ -29,6 +29,8 @@
 * xref:/manual/eventlogging.html[Event Logging]
 * xref:/manual/messages.html[Messages]
 * xref:/manual/thread-context.html[ThreadContext]
+* xref:/manual/scoped-context.html[ScopedContext]
+* xref:/manual/resource-logger.html[ResourceLogger]
 
 == Configuration
 
diff --git a/src/site/asciidoc/manual/resource-logger.adoc 
b/src/site/asciidoc/manual/resource-logger.adoc
new file mode 100644
index 0000000000..615e6f6e92
--- /dev/null
+++ b/src/site/asciidoc/manual/resource-logger.adoc
@@ -0,0 +1,93 @@
+////
+    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 API
+Ralph Goers <[email protected]>;
+
+== 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
+
+----
+%K %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
+----
+
+ResourceLoggers always create ParameterizedMapMessages for every log event.  A 
ParameterizedMapMessage is similar to a ParameterizedMessage but with a Map 
attached. Since ParameterizedMapMessage is a MapMessage all the tooling 
available
+in Layouts, Filters, and Lookups may be used.
+
+The supplier configured on the ResourceLogger 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/asciidoc/manual/scoped-context.adoc 
b/src/site/asciidoc/manual/scoped-context.adoc
new file mode 100644
index 0000000000..592945cf7e
--- /dev/null
+++ b/src/site/asciidoc/manual/scoped-context.adoc
@@ -0,0 +1,118 @@
+////
+    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 API
+Ralph Goers <[email protected]>;
+
+== 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

Reply via email to