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

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

commit 969f68ca03745849365811ffbdf7f1a2288e6995
Author: Ralph Goers <[email protected]>
AuthorDate: Sat Mar 16 17:58:02 2024 -0700

    2214 - Add support for ScopedContext
---
 .../apache/logging/log4j/ScopedContextTest.java    |  44 ++++
 .../org/apache/logging/log4j/ScopedContext.java    | 224 +++++++++++++++++++++
 .../org/apache/logging/log4j/package-info.java     |   2 +-
 .../apache/logging/log4j/simple/SimpleLogger.java  |   7 +-
 .../logging/log4j/core/ScopedContextTest.java      |  66 ++++++
 log4j-core-test/src/test/resources/log4j-list2.xml |  31 +++
 .../log4j/core/impl/ScopedContextDataProvider.java |  50 +++++
 .../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/manual/scoped-context.adoc       |  82 ++++++++
 src/site/site.xml                                  |   1 +
 12 files changed, 515 insertions(+), 4 deletions(-)

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..609ec0b5ad
--- /dev/null
+++ 
b/log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.hamcrest.Matchers.nullValue;
+
+import org.junit.jupiter.api.Test;
+
+public class ScopedContextTest {
+
+    @Test
+    public void testScope() {
+        ScopedContext.newInstance()
+                .where("key1", "Log4j2")
+                .run(() -> assertThat(ScopedContext.get("key1"), 
equalTo("Log4j2")));
+        ScopedContext.newInstance().where("key1", "value1").run(() -> {
+            assertThat(ScopedContext.get("key1"), equalTo("value1"));
+            ScopedContext.newInstance(true).where("key2", "value2").run(() -> {
+                assertThat(ScopedContext.get("key1"), equalTo("value1"));
+                assertThat(ScopedContext.get("key2"), equalTo("value2"));
+            });
+            ScopedContext.newInstance().where("key2", "value2").run(() -> {
+                assertThat(ScopedContext.get("key1"), nullValue());
+                assertThat(ScopedContext.get("key2"), equalTo("value2"));
+            });
+        });
+    }
+}
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..d320b708d6
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
@@ -0,0 +1,224 @@
+/*
+ * 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.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.function.Supplier;
+
+/**
+ * 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.
+ *
+ * @since 2.24.0
+ */
+public class ScopedContext {
+
+    private static final ThreadLocal<Deque<Map<String, Renderable>>> 
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 Map<String, Renderable> getContext() {
+        Deque<Map<String, Renderable>> stack = scopedContext.get();
+        if (stack != null && !stack.isEmpty()) {
+            return Collections.unmodifiableMap(stack.getFirst());
+        }
+        return Collections.emptyMap();
+    }
+
+    private static void addScopedContext(Map<String, Renderable> contextMap) {
+        Deque<Map<String, Renderable>> stack = scopedContext.get();
+        if (stack == null) {
+            stack = new ArrayDeque<>();
+            scopedContext.set(stack);
+        }
+        stack.addFirst(contextMap);
+    }
+
+    private static void removeScopedContext() {
+        Deque<Map<String, Renderable>> stack = scopedContext.get();
+        if (stack != null) {
+            if (!stack.isEmpty()) {
+                stack.removeFirst();
+            }
+            if (stack.isEmpty()) {
+                scopedContext.remove();
+            }
+        }
+    }
+
+    /**
+     * Return a new ScopedContext.
+     * @return the ScopedContext.
+     */
+    public static ScopedContext newInstance() {
+        return newInstance(false);
+    }
+
+    /**
+     * Return a new ScopedContext.
+     * @param inherit true if this context should inherit the values of its 
parent.
+     * @return the ScopedContext.
+     */
+    public static ScopedContext newInstance(boolean inherit) {
+        return new ScopedContext(inherit);
+    }
+
+    /**
+     * 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) {
+        Renderable renderable = getContext().get(key);
+        if (renderable != null) {
+            return (T) renderable.getObject();
+        } else {
+            return null;
+        }
+    }
+
+    private final Map<String, Renderable> contextMap = new HashMap<>();
+
+    private ScopedContext(boolean inherit) {
+        Map<String, Renderable> parent = ScopedContext.getContext();
+        if (inherit && !parent.isEmpty()) {
+            contextMap.putAll(parent);
+        }
+    }
+
+    /**
+     * Add all the values in the specified Map to the ScopedContext being 
constructed.
+     *
+     * @param map The Map to add to the ScopedContext being constructed.
+     * @return the ScopedContext being constructed.
+     */
+    public ScopedContext where(Map<String, Object> map) {
+        map.forEach(this::addObject);
+        return this;
+    }
+
+    /**
+     * 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 ScopedContext where(String key, Object value) {
+        addObject(key, value);
+        return this;
+    }
+
+    /**
+     * 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 ScopedContext where(String key, Supplier<Object> supplier) {
+        addObject(key, supplier.get());
+        return this;
+    }
+
+    private void addObject(String key, Object obj) {
+        if (obj != null) {
+            if (obj instanceof Renderable) {
+                contextMap.put(key, (Renderable) obj);
+            } else {
+                contextMap.put(key, new ObjectRenderable(obj));
+            }
+        }
+    }
+
+    /**
+     * 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) {
+        addScopedContext(contextMap);
+        try {
+            op.run();
+        } finally {
+            removeScopedContext();
+        }
+    }
+
+    /**
+     * 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<? extends R> op) throws Exception {
+        addScopedContext(contextMap);
+        try {
+            return op.call();
+        } finally {
+            removeScopedContext();
+        }
+    }
+
+    /**
+     * 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/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..2c46190517 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.getContext().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/core/ScopedContextTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ScopedContextTest.java
new file mode 100644
index 0000000000..994097a566
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/ScopedContextTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hasSize;
+
+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) {
+        final org.apache.logging.log4j.Logger logger = 
context.getLogger("org.apache.logging.log4j.scoped");
+        ScopedContext.newInstance().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.newInstance().where("key1", "value1").run(() -> {
+            logger.debug("Log message 1 will include key1");
+            ScopedContext.newInstance(true)
+                    .where("key2", "value2")
+                    .run(() -> logger.debug("Log message 2 will include key1 
and key2"));
+            ScopedContext.newInstance()
+                    .where("key2", "value2")
+                    .run(() -> logger.debug("Log message 2 will include 
key2"));
+        });
+        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));
+        expected = "{key2=value2}";
+        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/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..7e638a57a8
--- /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 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.getContext();
+        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/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 bbcc5b10ef..79d4278c24 100644
--- a/src/site/_release-notes/_2.x.x.adoc
+++ b/src/site/_release-notes/_2.x.x.adoc
@@ -31,6 +31,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 `com.fasterxml.jackson:jackson-bom` to version `2.17.0` 
(https://github.com/apache/logging-log4j2/pull/2372[2372])
 * Update `com.google.guava:guava` to version `33.1.0-jre` 
(https://github.com/apache/logging-log4j2/pull/2377[2377])
diff --git a/src/site/asciidoc/manual/scoped-context.adoc 
b/src/site/asciidoc/manual/scoped-context.adoc
new file mode 100644
index 0000000000..7f9afd2167
--- /dev/null
+++ b/src/site/asciidoc/manual/scoped-context.adoc
@@ -0,0 +1,82 @@
+////
+    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`.
+
+[source,java]
+----
+ScopedContext.newInstance()
+    .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.
+
+=== Nested ScopedContexts
+
+ScopedContexts may be nested. When creating a nested context the default 
behavior is to
+hide the key/value pairs of the parent context. The may be included by passing 
`true` to
+the newInstance method when creating the child context.
+
+
+[source,java]
+----
+        ScopedContext.newInstance().where("key1", "value1").run(() -> {
+            assertThat(ScopedContext.get("key1"), equalTo("value1"));
+            ScopedContext.newInstance(true).where("key2", "value2").run(() -> {
+                assertThat(ScopedContext.get("key1"), equalTo("value1"));
+                assertThat(ScopedContext.get("key2"), equalTo("value2"));
+            });
+            ScopedContext.newInstance().where("key2", "value2").run(() -> {
+                assertThat(ScopedContext.get("key1"), nullValue());
+                assertThat(ScopedContext.get("key2"), equalTo("value2"));
+            });
+        });
+
+----
\ No newline at end of file
diff --git a/src/site/site.xml b/src/site/site.xml
index 75c89e075e..b044e24a25 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -100,6 +100,7 @@
         <item name="Event Logging" href="/manual/eventlogging.html"/>
         <item name="Messages" href="/manual/messages.html"/>
         <item name="ThreadContext" href="/manual/thread-context.html"/>
+        <item name="ScopedContext" href="/manual/scoped-context.html"/>
       </item>
 
       <item name="Kotlin API" href="https://logging.apache.org/log4j/kotlin"/>

Reply via email to