You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2024/03/17 00:58:24 UTC
(logging-log4j2) 01/01: 2214 - Add support for ScopedContext
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 <rg...@apache.org>
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 <rg...@apache.org>;
+
+== 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"/>