From 193ed95fb5ef09f2fa1d0275f8eb6119425b740d Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Wed, 27 Mar 2024 13:46:40 -0700
Subject: [PATCH 01/19] Add ScopedContext and ResourceLogger
---
.../apache/logging/log4j/test/TestLogger.java | 6 +
.../logging/log4j/ResourceLoggerTest.java | 136 +++++
.../logging/log4j/ScopedContextTest.java | 154 +++++
.../message/ParameterizedMapMessageTest.java | 77 +++
.../message/ParameterizedMessageTest.java | 303 ----------
.../apache/logging/log4j/ResourceLogger.java | 276 +++++++++
.../apache/logging/log4j/ScopedContext.java | 558 ++++++++++++++++++
.../log4j/internal/ScopedContextAnchor.java | 69 +++
.../message/ParameterizedMapMessage.java | 38 ++
.../ParameterizedMapMessageFactory.java | 216 +++++++
.../logging/log4j/message/package-info.java | 2 +-
.../apache/logging/log4j/package-info.java | 2 +-
.../logging/log4j/simple/SimpleLogger.java | 7 +-
.../logging/log4j/ResourceLoggerTest.java | 158 +++++
.../logging/log4j/core/ScopedContextTest.java | 72 +++
.../src/test/resources/log4j-list2.xml | 31 +
.../src/test/resources/log4j-map.xml | 34 ++
.../core/impl/ScopedContextDataProvider.java | 50 ++
.../core/impl/internal/package-info.java | 25 +
.../logging/log4j/core/impl/package-info.java | 2 +-
src/changelog/.2.x.x/add_scoped_context.xml | 9 +
.../ROOT/pages/manual/resource-logger.adoc | 93 +++
.../ROOT/pages/manual/scoped-context.adoc | 118 ++++
23 files changed, 2128 insertions(+), 308 deletions(-)
create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java
create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMapMessageTest.java
delete mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageTest.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessage.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessageFactory.java
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/ScopedContextTest.java
create mode 100644 log4j-core-test/src/test/resources/log4j-list2.xml
create mode 100644 log4j-core-test/src/test/resources/log4j-map.xml
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
create mode 100644 src/changelog/.2.x.x/add_scoped_context.xml
create mode 100644 src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
create mode 100644 src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc
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 e95f1e9bb67..ff9c6f01c00 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.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 @@ protected void log(
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 00000000000..f706b0a7dd3
--- /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 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> {
+
+ private final Connection connection;
+
+ public MapSupplier(final Connection connection) {
+ this.connection = connection;
+ }
+
+ @Override
+ public Map get() {
+ Map 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 00000000000..d9ba5872e62
--- /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 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 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 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 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 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 00000000000..a560570846b
--- /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 4bd5df91bef..00000000000
--- 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 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:
- *
- * 1. The arguments contains an exception, and the count of placeholder is equal to arguments include exception.
- * 2. The arguments contains an exception, and the count of placeholder is equal to arguments except exception.
- * All of these should not logged in status logger.
- *
- *
- * @return Streams
- */
- static Stream 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 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 statusDataList = statusListener.getStatusData().collect(Collectors.toList());
- assertThat(statusDataList).hasSize(0);
- }
-
- /**
- * In this test cases, constructed the following scenarios:
- *
- * 1. The placeholders are greater than the count of arguments.
- * 2. The placeholders are less than the count of arguments.
- * 3. The arguments contains an exception, and the placeholder is greater than normal arguments.
- * 4. The arguments contains an exception, and the placeholder is less than the arguments.
- * All of these should logged in status logger with WARN level.
- *
- *
- * @return streams
- */
- static Stream 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 formattedMessageSupplier) {
-
- // Verify the formatted message
- final String formattedMessage = formattedMessageSupplier.get();
- assertThat(formattedMessage).isEqualTo(expected);
-
- // Verify the status logger warn
- final List 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 00000000000..bac943751c1
--- /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> 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> 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> 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> 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 00000000000..0806f820c55
--- /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 getContextMap() {
+ Optional 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 get(String key) {
+ Optional 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 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 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 map) {
+ if (map != null && !map.isEmpty()) {
+ Map 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 map = new HashMap<>();
+ if (current().isPresent()) {
+ map.putAll(current().get().contextMap);
+ }
+ map.put(key, renderable);
+ new Instance(map).run(op);
+ } else {
+ Map 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 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 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 map, Runnable op) {
+ if (map != null && !map.isEmpty()) {
+ Map 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 callWhere(String key, Object obj, Callable op) throws Exception {
+ if (obj != null) {
+ Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
+ Map map = new HashMap<>();
+ if (current().isPresent()) {
+ map.putAll(current().get().contextMap);
+ }
+ map.put(key, renderable);
+ return new Instance(map).call(op);
+ } else {
+ Map 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 Future callWhere(String key, Object obj, ExecutorService executorService, Callable op)
+ throws Exception {
+ if (obj != null) {
+ Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
+ Map map = new HashMap<>();
+ if (current().isPresent()) {
+ map.putAll(current().get().contextMap);
+ }
+ map.put(key, renderable);
+ if (executorService != null) {
+ return executorService.submit(new Caller(
+ 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 map = new HashMap<>();
+ if (current().isPresent()) {
+ map.putAll(current().get().contextMap);
+ }
+ map.remove(key);
+ return executorService.submit(new Caller(
+ 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 callWhere(Map map, Callable op) throws Exception {
+ if (map != null && !map.isEmpty()) {
+ Map 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 current() {
+ return ScopedContextAnchor.getContext();
+ }
+
+ public static class Instance {
+
+ private final Instance parent;
+ private final String key;
+ private final Renderable value;
+ private final Map contextMap;
+
+ private Instance() {
+ this.parent = null;
+ this.key = null;
+ this.value = null;
+ this.contextMap = null;
+ }
+
+ private Instance(Map 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 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 call(Callable op) throws Exception {
+ return new Caller(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 Future call(ExecutorService executorService, Callable op) {
+ return executorService.submit(
+ new Caller(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), op));
+ }
+ }
+
+ private static class Runner implements Runnable {
+ private final Map contextMap = new HashMap<>();
+ private final Map threadContextMap;
+ private final ThreadContext.ContextStack contextStack;
+ private final Instance context;
+ private final Runnable op;
+
+ public Runner(
+ Instance context,
+ Map 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 implements Callable {
+ private final Map contextMap = new HashMap<>();
+ private final Instance context;
+ private final Map threadContextMap;
+ private final ThreadContext.ContextStack contextStack;
+ private final Callable op;
+
+ public Caller(
+ Instance context,
+ Map threadContextMap,
+ ThreadContext.ContextStack contextStack,
+ Callable 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 00000000000..c09c4bc78ff
--- /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> 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 getContext() {
+ Deque 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 stack = scopedContext.get();
+ if (stack == null) {
+ stack = new ArrayDeque<>();
+ scopedContext.set(stack);
+ }
+ stack.addFirst(context);
+ }
+
+ /**
+ * Remove the top ScopeContext.
+ */
+ public static void removeScopedContext() {
+ Deque 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 00000000000..292bbb8290b
--- /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 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 00000000000..48575c08499
--- /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> mapSupplier;
+
+ public ParameterizedMapMessageFactory(Supplier> mapSupplier) {
+ this.mapSupplier = mapSupplier;
+ }
+
+ @Override
+ public Message newMessage(final CharSequence message) {
+ Map map = mapSupplier.get();
+ Message msg = new SimpleMessage(message);
+ return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+ }
+
+ @Override
+ public Message newMessage(final Object message) {
+ Map map = mapSupplier.get();
+ Message msg = new ObjectMessage(message);
+ return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
+ }
+
+ @Override
+ public Message newMessage(final String message) {
+ Map 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 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 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 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 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 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 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 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 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 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 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 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 24632f8d7b5..393e7b517fc 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 5407f05f619..f1c67c6c86c 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 Log4j 2 API manual
*/
@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 1690893187f..f5529f4258d 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.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 void logMessage(
}
sb.append(msg.getFormattedMessage());
if (showContextMap) {
- final Map mdc = ThreadContext.getImmutableContext();
- if (mdc.size() > 0) {
+ final Map 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 00000000000..b62784b4c7d
--- /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 events = app.getEvents();
+ assertThat(events, hasSize(1));
+ Message message = events.get(0).getMessage();
+ assertTrue(message instanceof ParameterizedMapMessage);
+ Map 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> {
+
+ private final Connection connection;
+
+ public MapSupplier(final Connection connection) {
+ this.connection = connection;
+ }
+
+ @Override
+ public Map get() {
+ Map 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 00000000000..99538081578
--- /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 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 00000000000..c747458fbdb
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-list2.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 00000000000..1167c7cc6e2
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-map.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 00000000000..a4f651b7ea1
--- /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} data.
+ */
+@ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
+public class ScopedContextDataProvider implements ContextDataProvider {
+
+ @Override
+ public Map supplyContextData() {
+ Map contextMap = ScopedContext.getContextMap();
+ if (!contextMap.isEmpty()) {
+ Map 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/internal/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
new file mode 100644
index 00000000000..9d76948fcaf
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+/**
+ * Log4j 2 private implementation classes.
+ */
+@Export
+@Version("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 c50504a8726..0c3b08f43a7 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 00000000000..06db3eb0d54
--- /dev/null
+++ b/src/changelog/.2.x.x/add_scoped_context.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ Add ScopedContext to log4j-api and ScopedContextDataProvider in log4j-core.
+
diff --git a/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc b/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
new file mode 100644
index 00000000000..615e6f6e92b
--- /dev/null
+++ b/src/site/antora/modules/ROOT/pages/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 ;
+
+== 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> {
+
+ public Map get() {
+ Map 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/antora/modules/ROOT/pages/manual/scoped-context.adoc b/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc
new file mode 100644
index 00000000000..592945cf7e9
--- /dev/null
+++ b/src/site/antora/modules/ROOT/pages/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 ;
+
+== 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 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
From 86aac2019b2dc09e3af0713ea3c02e2855179ac4 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Fri, 29 Mar 2024 07:49:49 -0700
Subject: [PATCH 02/19] ResourceLogger uses ScopedContext
---
.../apache/logging/log4j/test/TestLogger.java | 10 ++++--
.../logging/log4j/ResourceLoggerTest.java | 4 +--
.../apache/logging/log4j/ResourceLogger.java | 35 ++++++++++++++-----
.../apache/logging/log4j/ScopedContext.java | 10 +++---
.../logging/log4j/ResourceLoggerTest.java | 30 ++++++++--------
.../ROOT/pages/manual/resource-logger.adoc | 7 ++--
6 files changed, 60 insertions(+), 36 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 ff9c6f01c00..d90258e5a2b 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
@@ -20,10 +20,12 @@
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
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;
@@ -80,14 +82,18 @@ protected void log(
sb.append(' ');
}
sb.append(message.getFormattedMessage());
- final Map mdc = ThreadContext.getImmutableContext();
+ Map contextMap = ScopedContext.getContextMap();
+ final Map mdc = new HashMap<>(ThreadContext.getImmutableContext());
+ if (contextMap != null && !contextMap.isEmpty()) {
+ contextMap.forEach((key, value) -> mdc.put(key, value.render()));
+ }
if (!mdc.isEmpty()) {
sb.append(' ');
sb.append(mdc);
sb.append(' ');
}
if (message instanceof ParameterizedMapMessage) {
- sb.append(" Resource data: ");
+ sb.append(" Map data: ");
sb.append(((ParameterizedMapMessage) message).getData().toString());
sb.append(' ');
}
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
index f706b0a7dd3..c9a8ae691ed 100644
--- 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
@@ -78,7 +78,7 @@ public void testFactory() throws Exception {
events.clear();
}
- private static class MapSupplier implements Supplier> {
+ private static class MapSupplier implements Supplier> {
private final Connection connection;
@@ -87,7 +87,7 @@ public MapSupplier(final Connection connection) {
}
@Override
- public Map get() {
+ public Map get() {
Map map = new HashMap<>();
map.put("Name", connection.name);
map.put("Type", connection.type);
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
index bac943751c1..6df94e93344 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
@@ -20,7 +20,7 @@
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.message.MessageFactory;
import org.apache.logging.log4j.spi.AbstractLogger;
import org.apache.logging.log4j.spi.ExtendedLogger;
import org.apache.logging.log4j.status.StatusLogger;
@@ -40,6 +40,7 @@
public final class ResourceLogger extends AbstractLogger {
private static final long serialVersionUID = -5837924138744974513L;
private final ExtendedLogger logger;
+ private final Supplier> supplier;
public static ResourceLoggerBuilder newBuilder() {
return new ResourceLoggerBuilder();
@@ -49,9 +50,11 @@ public static ResourceLoggerBuilder newBuilder() {
* 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> supplier) {
- super(logger.getName(), new ParameterizedMapMessageFactory(supplier));
+ private ResourceLogger(
+ final ExtendedLogger logger, final Supplier> supplier, MessageFactory messageFactory) {
+ super(logger.getName(), messageFactory);
this.logger = logger;
+ this.supplier = supplier;
}
@Override
@@ -197,7 +200,11 @@ public boolean isEnabled(
@Override
public void logMessage(String fqcn, Level level, Marker marker, Message message, Throwable t) {
- logger.logMessage(fqcn, level, marker, message, t);
+ if (supplier != null) {
+ ScopedContext.runWhere(supplier.get(), () -> logger.logMessage(fqcn, level, marker, message, t));
+ } else {
+ logger.logMessage(fqcn, level, marker, message, t);
+ }
}
/**
@@ -207,7 +214,8 @@ public static final class ResourceLoggerBuilder {
private static final Logger LOGGER = StatusLogger.getLogger();
private ExtendedLogger logger;
private String name;
- private Supplier> supplier;
+ private Supplier> supplier;
+ private MessageFactory messageFactory;
/**
* Create the builder.
@@ -252,11 +260,22 @@ public ResourceLoggerBuilder withClass(Class> clazz) {
* @param supplier the method that provides the Map of resource data to include in logs.
* @return the ResourceLoggerBuilder.
*/
- public ResourceLoggerBuilder withSupplier(Supplier> supplier) {
+ public ResourceLoggerBuilder withSupplier(Supplier> supplier) {
this.supplier = supplier;
return this;
}
+ /**
+ * Adds a MessageFactory.
+ * @param messageFactory the MessageFactory to use to build messages. If a MessageFactory
+ * is not specified the default MessageFactory will be used.
+ * @return the ResourceLoggerBuilder.
+ */
+ public ResourceLoggerBuilder withMessageFactory(MessageFactory messageFactory) {
+ this.messageFactory = messageFactory;
+ return this;
+ }
+
/**
* Construct the ResourceLogger.
* @return the ResourceLogger.
@@ -269,8 +288,8 @@ public ResourceLogger build() {
}
this.logger = (ExtendedLogger) LogManager.getLogger(name);
}
- Supplier> mapSupplier = this.supplier != null ? this.supplier : Collections::emptyMap;
- return new ResourceLogger(logger, mapSupplier);
+ Supplier> mapSupplier = this.supplier != null ? this.supplier : Collections::emptyMap;
+ return new ResourceLogger(logger, mapSupplier, messageFactory);
}
}
}
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
index 0806f820c55..b105e3be1d1 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
@@ -124,7 +124,7 @@ public static Instance where(String key, Supplier supplier) {
* @param map the Map.
* @return the ScopedContext Instance constructed.
*/
- public static Instance where(Map map) {
+ public static Instance where(Map map) {
if (map != null && !map.isEmpty()) {
Map renderableMap = new HashMap<>();
if (current().isPresent()) {
@@ -212,11 +212,11 @@ public static Future> runWhere(String key, Object obj, ExecutorService executo
* @param map the Map.
* @param op the Runnable to call.
*/
- public static void runWhere(Map map, Runnable op) {
+ public static void runWhere(Map map, Runnable op) {
if (map != null && !map.isEmpty()) {
Map renderableMap = new HashMap<>();
if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ renderableMap.putAll(current().get().contextMap);
}
map.forEach((key, value) -> {
renderableMap.put(key, value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value));
@@ -296,11 +296,11 @@ public static Future callWhere(String key, Object obj, ExecutorService ex
* @param map the Map.
* @param op the Runnable to call.
*/
- public static R callWhere(Map map, Callable op) throws Exception {
+ public static R callWhere(Map map, Callable op) throws Exception {
if (map != null && !map.isEmpty()) {
Map renderableMap = new HashMap<>();
if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ renderableMap.putAll(current().get().contextMap);
}
map.forEach((key, value) -> {
renderableMap.put(key, value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value));
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
index b62784b4c7d..a66f4459ac6 100644
--- 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
@@ -18,9 +18,10 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
+import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.HashMap;
import java.util.List;
@@ -33,6 +34,7 @@
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.apache.logging.log4j.util.ReadOnlyStringMap;
import org.junit.jupiter.api.Test;
/**
@@ -59,14 +61,14 @@ public void testFactory(final LoggerContext context) throws Exception {
logger.debug("Hello, {}", "World");
List events = app.getEvents();
assertThat(events, hasSize(1));
- Message message = events.get(0).getMessage();
- assertTrue(message instanceof ParameterizedMapMessage);
- Map data = ((ParameterizedMapMessage) message).getData();
- assertThat(data, aMapWithSize(3));
+ ReadOnlyStringMap map = events.get(0).getContextData();
+ assertNotNull(map);
+ Map data = map.toMap();
+ assertThat(data.size(), equalTo(3));
assertEquals("Test", data.get("Name"));
assertEquals("dummy", data.get("Type"));
assertEquals("1", data.get("Count"));
- assertEquals("Hello, World", message.getFormattedMessage());
+ assertEquals("Hello, World", events.get(0).getMessage().getFormattedMessage());
assertEquals(this.getClass().getName(), events.get(0).getLoggerName());
assertEquals(this.getClass().getName(), events.get(0).getSource().getClassName());
app.clear();
@@ -74,9 +76,9 @@ public void testFactory(final LoggerContext context) throws Exception {
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();
+ map = events.get(0).getContextData();
+ assertNotNull(map);
+ data = map.toMap();
assertThat(data, aMapWithSize(3));
assertEquals("2", data.get("Count"));
app.clear();
@@ -87,20 +89,20 @@ public void testFactory(final LoggerContext context) throws Exception {
logger.debug("Connection: {}", "NewConnection");
events = app.getEvents();
assertThat(events, hasSize(1));
- message = events.get(0).getMessage();
- assertTrue(message instanceof ParameterizedMapMessage);
- data = ((ParameterizedMapMessage) message).getData();
+ map = events.get(0).getContextData();
+ assertNotNull(map);
+ data = map.toMap();
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("Connection: NewConnection", events.get(0).getMessage().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> {
+ private static class MapSupplier implements Supplier> {
private final Connection connection;
diff --git a/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc b/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
index 615e6f6e92b..289b69a3443 100644
--- a/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
@@ -78,7 +78,7 @@ inside the Resource Logger.
With the PatternLayout configured with a pattern of
----
-%K %m%n
+%X %m%n
----
and a loginId of testUser and a role of Admin, after a successful login would result in a log message of
@@ -87,7 +87,4 @@ and a loginId of testUser and a role of Admin, after a successful login would re
{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.
+Every logging call is wrapped in a ScopedContext and populated by the supplier configured on the ResourceLogger, which is called when generating every log event. This allows values, such as counters, to be updated and the log event will contain the actual value at the time the event was logged.
From 63765d148a1d0ea81869ea0777964d7fb53e7c50 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Fri, 29 Mar 2024 08:09:08 -0700
Subject: [PATCH 03/19] Use ExtendedLoggerWrapper
---
.../apache/logging/log4j/ResourceLogger.java | 149 +-----------------
1 file changed, 3 insertions(+), 146 deletions(-)
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
index 6df94e93344..17320a74af6 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
@@ -21,8 +21,8 @@
import java.util.function.Supplier;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.MessageFactory;
-import org.apache.logging.log4j.spi.AbstractLogger;
import org.apache.logging.log4j.spi.ExtendedLogger;
+import org.apache.logging.log4j.spi.ExtendedLoggerWrapper;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.StackLocatorUtil;
import org.apache.logging.log4j.util.Strings;
@@ -37,9 +37,8 @@
* 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 {
+public final class ResourceLogger extends ExtendedLoggerWrapper {
private static final long serialVersionUID = -5837924138744974513L;
- private final ExtendedLogger logger;
private final Supplier> supplier;
public static ResourceLoggerBuilder newBuilder() {
@@ -52,152 +51,10 @@ public static ResourceLoggerBuilder newBuilder() {
*/
private ResourceLogger(
final ExtendedLogger logger, final Supplier> supplier, MessageFactory messageFactory) {
- super(logger.getName(), messageFactory);
- this.logger = logger;
+ super(logger, logger.getName(), messageFactory);
this.supplier = supplier;
}
- @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) {
if (supplier != null) {
From 44b19b0e8a2a8f2c30fdd1a72a238c13ed306bf4 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Wed, 3 Apr 2024 08:19:52 -0700
Subject: [PATCH 04/19] Move ContextDataProviders to the API
---
.../apache/logging/log4j/test/TestLogger.java | 10 +-
.../logging/log4j/ResourceLoggerTest.java | 7 +-
.../org/apache/logging/log4j/ContextData.java | 106 +++++++
.../apache/logging/log4j/ResourceLogger.java | 5 +-
.../apache/logging/log4j/ScopedContext.java | 62 +++-
.../logging/log4j/simple/SimpleLogger.java | 7 +-
.../log4j/spi/ContextDataProvider.java | 76 +++++
.../ScopedContextDataProvider.java} | 41 ++-
.../log4j/spi/ThreadContextDataProvider.java | 35 ++-
.../logging/log4j/ResourceLoggerTest.java | 3 +-
.../impl/ThreadContextDataInjectorTest.java | 2 +-
.../log4j/core/ContextDataInjector.java | 16 +-
.../logging/log4j/core/async/AsyncLogger.java | 10 +-
.../log4j/core/async/RingBufferLogEvent.java | 4 +-
.../async/RingBufferLogEventTranslator.java | 10 +-
.../core/filter/DynamicThresholdFilter.java | 19 +-
.../core/filter/ThreadContextMapFilter.java | 25 +-
.../log4j/core/filter/package-info.java | 2 +-
.../core/impl/ContextDataInjectorFactory.java | 25 +-
.../core/impl/JdkMapAdapterStringMap.java | 2 +-
.../log4j/core/impl/Log4jLogEvent.java | 21 ++
.../core/impl/ReusableLogEventFactory.java | 24 +-
.../core/impl/ThreadContextDataInjector.java | 284 +++++++++---------
.../log4j/core/lookup/ContextMapLookup.java | 18 +-
.../log4j/core/lookup/package-info.java | 2 +-
.../logging/log4j/core/osgi/Activator.java | 8 +-
.../logging/log4j/core/package-info.java | 2 +-
.../log4j/core/util/ContextDataProvider.java | 14 +-
.../logging/log4j/core/util/package-info.java | 2 +-
29 files changed, 587 insertions(+), 255 deletions(-)
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
rename log4j-api/src/main/java/org/apache/logging/log4j/{internal/ScopedContextAnchor.java => spi/ScopedContextDataProvider.java} (66%)
rename log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java => log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java (56%)
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 d90258e5a2b..2387fee7e6a 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
@@ -23,10 +23,9 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.logging.log4j.ContextData;
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;
import org.apache.logging.log4j.message.ParameterizedMapMessage;
@@ -82,11 +81,8 @@ protected void log(
sb.append(' ');
}
sb.append(message.getFormattedMessage());
- Map contextMap = ScopedContext.getContextMap();
- final Map mdc = new HashMap<>(ThreadContext.getImmutableContext());
- if (contextMap != null && !contextMap.isEmpty()) {
- contextMap.forEach((key, value) -> mdc.put(key, value.render()));
- }
+ final Map mdc = new HashMap<>(ContextData.size());
+ ContextData.addAll(mdc);
if (!mdc.isEmpty()) {
sb.append(' ');
sb.append(mdc);
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
index c9a8ae691ed..4822a52e123 100644
--- 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
@@ -40,12 +40,17 @@ public static void beforeAll() {
System.setProperty("log4j2.loggerContextFactory", TestLoggerContextFactory.class.getName());
}
+ @BeforeAll
+ public static void afterAll() {
+ System.clearProperty("log4j2.loggerContextFactory");
+ }
+
@Test
public void testFactory() throws Exception {
Connection connection = new Connection("Test", "dummy");
connection.useConnection();
MapSupplier mapSupplier = new MapSupplier(connection);
- ResourceLogger logger = ResourceLogger.newBuilder()
+ Logger logger = ResourceLogger.newBuilder()
.withClass(this.getClass())
.withSupplier(mapSupplier)
.build();
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java b/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java
new file mode 100644
index 00000000000..1b9445dcd80
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java
@@ -0,0 +1,106 @@
+/*
+ * 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.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.logging.log4j.spi.ContextDataProvider;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.ServiceLoaderUtil;
+import org.apache.logging.log4j.util.StringMap;
+
+/**
+ * General purpose utility class for accessing data accessible through ContextDataProviders.
+ */
+public final class ContextData {
+
+ private static final Logger LOGGER = StatusLogger.getLogger();
+ /**
+ * ContextDataProviders loaded via OSGi.
+ */
+ public static Collection contextDataProviders = new ConcurrentLinkedDeque<>();
+
+ private static final List SERVICE_PROVIDERS = getServiceProviders();
+
+ private ContextData() {}
+
+ private static List getServiceProviders() {
+ final List providers = new ArrayList<>();
+ ServiceLoaderUtil.safeStream(
+ ContextDataProvider.class,
+ ServiceLoader.load(ContextDataProvider.class, ContextData.class.getClassLoader()),
+ LOGGER)
+ .forEach(providers::add);
+ return Collections.unmodifiableList(providers);
+ }
+
+ public static void addProvider(ContextDataProvider provider) {
+ contextDataProviders.add(provider);
+ }
+
+ private static List getProviders() {
+ final List providers =
+ new ArrayList<>(contextDataProviders.size() + SERVICE_PROVIDERS.size());
+ providers.addAll(contextDataProviders);
+ providers.addAll(SERVICE_PROVIDERS);
+ return providers;
+ }
+
+ public static int size() {
+ final List providers = getProviders();
+ final AtomicInteger count = new AtomicInteger(0);
+ providers.forEach((provider) -> count.addAndGet(provider.size()));
+ return count.get();
+ }
+
+ /**
+ * Populates the provided StringMap with data from the Context.
+ * @param stringMap the StringMap to contain the results.
+ */
+ public static void addAll(StringMap stringMap) {
+ final List providers = getProviders();
+ providers.forEach((provider) -> provider.addAll(stringMap));
+ }
+
+ /**
+ * Populates the provided Map with data from the Context.
+ * @param map the Map to contain the results.
+ * @return the Map. Useful for chaining operations.
+ */
+ public static Map addAll(Map map) {
+ final List providers = getProviders();
+ providers.forEach((provider) -> provider.addAll(map));
+ return map;
+ }
+
+ public static String getValue(String key) {
+ List providers = getProviders();
+ for (ContextDataProvider provider : providers) {
+ String value = provider.get(key);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+}
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
index 17320a74af6..7a30dab94d9 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java
@@ -28,8 +28,7 @@
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
+ * Logger for resources. 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.
@@ -137,7 +136,7 @@ public ResourceLoggerBuilder withMessageFactory(MessageFactory messageFactory) {
* Construct the ResourceLogger.
* @return the ResourceLogger.
*/
- public ResourceLogger build() {
+ public Logger build() {
if (this.logger == null) {
if (Strings.isEmpty(name)) {
Class> clazz = StackLocatorUtil.getCallerClass(2);
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
index b105e3be1d1..67e2ba97fde 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
@@ -25,7 +25,7 @@
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.spi.ScopedContextDataProvider;
import org.apache.logging.log4j.status.StatusLogger;
/**
@@ -59,7 +59,7 @@ public class ScopedContext {
* @return the Map of Renderable objects.
*/
public static Map getContextMap() {
- Optional context = ScopedContextAnchor.getContext();
+ Optional context = ScopedContextDataProvider.getContext();
if (context.isPresent()
&& context.get().contextMap != null
&& !context.get().contextMap.isEmpty()) {
@@ -69,13 +69,23 @@ public static Map getContextMap() {
}
/**
- * Return the key from the current ScopedContext, if there is one and the key exists.
+ * @hidden
+ * Returns the number of entries in the context map.
+ * @return the number of items in the context map.
+ */
+ public static int size() {
+ Optional context = ScopedContextDataProvider.getContext();
+ return context.map(instance -> instance.contextMap.size()).orElse(0);
+ }
+
+ /**
+ * Return the value of 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 get(String key) {
- Optional context = ScopedContextAnchor.getContext();
+ Optional context = ScopedContextDataProvider.getContext();
if (context.isPresent()) {
Renderable renderable = context.get().contextMap.get(key);
if (renderable != null) {
@@ -85,6 +95,36 @@ public static T get(String key) {
return null;
}
+ /**
+ * Return String value of 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.
+ */
+ public static String getString(String key) {
+ Optional context = ScopedContextDataProvider.getContext();
+ if (context.isPresent()) {
+ Renderable renderable = context.get().contextMap.get(key);
+ if (renderable != null) {
+ return renderable.render();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds all the String rendered objects in the context map to the provided Map.
+ * @param map The Map to add entries to.
+ */
+ public static void addAll(Map map) {
+ Optional context = ScopedContextDataProvider.getContext();
+ if (context.isPresent()) {
+ Map contextMap = context.get().contextMap;
+ if (contextMap != null && !contextMap.isEmpty()) {
+ contextMap.forEach((key, value) -> map.put(key, value.render()));
+ }
+ }
+ }
+
/**
* Creates a ScopedContext Instance with a key/value pair.
*
@@ -316,7 +356,7 @@ public static R callWhere(Map map, Callable op) throws Excepti
* @return an Optional containing the active ScopedContext, if there is one.
*/
private static Optional current() {
- return ScopedContextAnchor.getContext();
+ return ScopedContextDataProvider.getContext();
}
public static class Instance {
@@ -460,11 +500,11 @@ public void run() {
if (contextStack != null) {
ThreadContext.setStack(contextStack);
}
- ScopedContextAnchor.addScopedContext(scopedContext);
+ ScopedContextDataProvider.addScopedContext(scopedContext);
try {
op.run();
} finally {
- ScopedContextAnchor.removeScopedContext();
+ ScopedContextDataProvider.removeScopedContext();
ThreadContext.clearAll();
}
}
@@ -511,11 +551,11 @@ public R call() throws Exception {
if (contextStack != null) {
ThreadContext.setStack(contextStack);
}
- ScopedContextAnchor.addScopedContext(scopedContext);
+ ScopedContextDataProvider.addScopedContext(scopedContext);
try {
return op.call();
} finally {
- ScopedContextAnchor.removeScopedContext();
+ ScopedContextDataProvider.removeScopedContext();
ThreadContext.clearAll();
}
}
@@ -523,6 +563,10 @@ public R call() throws Exception {
/**
* Interface for converting Objects stored in the ContextScope to Strings for logging.
+ *
+ * Users implementing this interface are encouraged to make the render method as lightweight as possible,
+ * Typically by creating the String representation of the object during its construction and just returning
+ * the String.
*/
public static interface Renderable {
/**
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 f5529f4258d..d914f1c4c93 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
@@ -23,10 +23,9 @@
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
+import org.apache.logging.log4j.ContextData;
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;
import org.apache.logging.log4j.spi.AbstractLogger;
@@ -296,8 +295,8 @@ public void logMessage(
}
sb.append(msg.getFormattedMessage());
if (showContextMap) {
- final Map mdc = new HashMap<>(ThreadContext.getImmutableContext());
- ScopedContext.getContextMap().forEach((key, value) -> mdc.put(key, value.render()));
+ final Map mdc = new HashMap<>(ContextData.size());
+ ContextData.addAll(mdc);
if (!mdc.isEmpty()) {
sb.append(SPACE);
sb.append(mdc.toString());
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
new file mode 100644
index 00000000000..d003e9c74f1
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
@@ -0,0 +1,76 @@
+/*
+ * 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.spi;
+
+import java.util.Map;
+import org.apache.logging.log4j.util.StringMap;
+
+/**
+ * Source of context data to be added to each log event.
+ */
+public interface ContextDataProvider {
+
+ /**
+ * Returns the key for a value from the context data.
+ * @param key the key to locate.
+ * @return the value or null if it is not found.
+ */
+ default String get(String key) {
+ return null;
+ }
+
+ /**
+ * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
+ *
+ * Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
+ * underlying context data will not be reflected in the returned object.
+ *
+ * @return A Map containing the context data or null.
+ */
+ Map supplyContextData();
+
+ /**
+ * Returns the number of items in this context.
+ * @return the number of items in the context.
+ */
+ default int size() {
+ Map contextMap = supplyContextData();
+ return contextMap != null ? contextMap.size() : 0;
+ }
+
+ /**
+ * Add all the keys in the current context to the provided Map.
+ * @param map the StringMap to add the keys and values to.
+ */
+ default void addAll(Map map) {
+ Map contextMap = supplyContextData();
+ if (contextMap != null) {
+ map.putAll(contextMap);
+ }
+ }
+
+ /**
+ * Add all the keys in the current context to the provided StringMap.
+ * @param map the StringMap to add the keys and values to.
+ */
+ default void addAll(StringMap map) {
+ Map contextMap = supplyContextData();
+ if (contextMap != null) {
+ contextMap.forEach(map::putValue);
+ }
+ }
+}
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/spi/ScopedContextDataProvider.java
similarity index 66%
rename from log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
index c09c4bc78ff..f2a14c19d0b 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ScopedContextAnchor.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
@@ -14,17 +14,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.internal;
+package org.apache.logging.log4j.spi;
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.spi.ServiceProvider;
import java.util.ArrayDeque;
+import java.util.Collections;
import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Optional;
import org.apache.logging.log4j.ScopedContext;
/**
- * Anchor for the ScopedContext. This class is private and not for public consumption.
+ * ContextDataProvider for {@code Map} data.
+ * @since 2.24.0
*/
-public class ScopedContextAnchor {
+@ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
+public class ScopedContextDataProvider implements ContextDataProvider {
+
private static final ThreadLocal> scopedContext = new ThreadLocal<>();
/**
@@ -66,4 +74,31 @@ public static void removeScopedContext() {
}
}
}
+
+ @Override
+ public String get(String key) {
+ return ScopedContext.getString(key);
+ }
+
+ @Override
+ public Map supplyContextData() {
+ Map contextMap = ScopedContext.getContextMap();
+ if (!contextMap.isEmpty()) {
+ Map map = new HashMap<>();
+ contextMap.forEach((key, value) -> map.put(key, value.render()));
+ return map;
+ } else {
+ return Collections.emptyMap();
+ }
+ }
+
+ @Override
+ public int size() {
+ return ScopedContext.size();
+ }
+
+ @Override
+ public void addAll(Map map) {
+ ScopedContext.addAll(map);
+ }
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
similarity index 56%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
index a4f651b7ea1..d6e28f00d85 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
@@ -14,37 +14,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.core.impl;
+package org.apache.logging.log4j.spi;
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;
+import org.apache.logging.log4j.ThreadContext;
/**
- * ContextDataProvider for {@code Map} data.
+ * ContextDataProvider for ThreadContext data.
*/
@ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
-public class ScopedContextDataProvider implements ContextDataProvider {
+public class ThreadContextDataProvider implements ContextDataProvider {
+
+ @Override
+ public String get(String key) {
+ return ThreadContext.get(key);
+ }
@Override
public Map supplyContextData() {
- Map contextMap = ScopedContext.getContextMap();
- if (!contextMap.isEmpty()) {
- Map map = new HashMap<>();
- contextMap.forEach((key, value) -> map.put(key, value.render()));
- return map;
- } else {
- return Collections.emptyMap();
- }
+ return ThreadContext.getImmutableContext();
+ }
+
+ @Override
+ public int size() {
+ return ThreadContext.getContext().size();
}
@Override
- public StringMap supplyStringMap() {
- return new JdkMapAdapterStringMap(supplyContextData());
+ public void addAll(Map map) {
+ map.putAll(ThreadContext.getContext());
}
}
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
index a66f4459ac6..738abeec7be 100644
--- 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
@@ -28,6 +28,7 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
+import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ResourceLogger;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
@@ -54,7 +55,7 @@ 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()
+ Logger logger = ResourceLogger.newBuilder()
.withClass(this.getClass())
.withSupplier(mapSupplier)
.build();
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
index 39258f5003e..94654608239 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjectorTest.java
@@ -91,7 +91,7 @@ private void testContextDataInjector() {
: readOnlythreadContextMap.getClass().getName(),
is(equalTo(readOnlythreadContextMapClassName)));
- final ContextDataInjector contextDataInjector = createInjector();
+ final ContextDataInjector contextDataInjector = createInjector(true);
final StringMap stringMap = contextDataInjector.injectContextData(null, new SortedArrayStringMap());
assertThat("thread context map", ThreadContext.getContext(), allOf(hasEntry("foo", "bar"), not(hasKey("baz"))));
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
index 6d386fe139f..56293f59c10 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/ContextDataInjector.java
@@ -21,6 +21,7 @@
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
import org.apache.logging.log4j.util.StringMap;
/**
@@ -105,6 +106,19 @@ public interface ContextDataInjector {
* the implementation of this method. It is not safe to pass the returned object to another thread.
*
* @return a {@code ReadOnlyStringMap} object reflecting the current state of the context, may not return {@code null}
+ * @deprecated - Methods using this have been converted to call getValue(). Will be removed in 3.0.0.
*/
- ReadOnlyStringMap rawContextData();
+ @Deprecated
+ default ReadOnlyStringMap rawContextData() {
+ return new SortedArrayStringMap();
+ }
+
+ /**
+ * Retrieves a key from the context. This avoids having to construct a composite Map when multiple contexts are available.
+ * @param key the key to retrieve.
+ * @return the String value associated with the key.
+ */
+ default String getValue(String key) {
+ return null;
+ }
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
index 0378d010ccc..5bb479f66ce 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
@@ -19,6 +19,7 @@
import com.lmax.disruptor.EventTranslatorVararg;
import com.lmax.disruptor.dsl.Disruptor;
import java.util.List;
+import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
@@ -484,6 +485,11 @@ public void translateTo(final RingBufferLogEvent event, final long sequence, fin
final Thread currentThread = Thread.currentThread();
final String threadName = THREAD_NAME_CACHING_STRATEGY.getThreadName();
+ if (CONTEXT_DATA_INJECTOR == null) {
+ ContextData.addAll((StringMap) event.getContextData());
+ } else {
+ CONTEXT_DATA_INJECTOR.injectContextData(null, (StringMap) event.getContextData());
+ }
event.setValues(
asyncLogger,
asyncLogger.getName(),
@@ -492,9 +498,7 @@ public void translateTo(final RingBufferLogEvent event, final long sequence, fin
level,
message,
thrown,
- // config properties are taken care of in the EventHandler thread
- // in the AsyncLogger#actualAsyncLog method
- CONTEXT_DATA_INJECTOR.injectContextData(null, (StringMap) event.getContextData()),
+ null,
contextStack,
currentThread.getId(),
threadName,
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java
index d3ab5a10e8d..ae8e1bce940 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEvent.java
@@ -121,7 +121,9 @@ public void setValues(
this.marker = aMarker;
this.fqcn = theFqcn;
this.location = aLocation;
- this.contextData = mutableContextData;
+ if (mutableContextData != null) {
+ this.contextData = mutableContextData;
+ }
this.contextStack = aContextStack;
this.asyncLogger = anAsyncLogger;
this.populated = true;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
index 9763ff7fce4..c965ce974d6 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
@@ -17,6 +17,7 @@
package org.apache.logging.log4j.core.async;
import com.lmax.disruptor.EventTranslator;
+import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext.ContextStack;
@@ -57,6 +58,13 @@ public class RingBufferLogEventTranslator implements EventTranslator> pairs, final boolean oper, final Result onMatch, final Result onMismatch) {
super(pairs, oper, onMatch, onMismatch);
- // ContextDataFactory looks up a property. The Spring PropertySource may log which will cause recursion.
- // By initializing the ContextDataFactory here recursion will be prevented.
- final StringMap map = ContextDataFactory.createContextData();
- LOGGER.debug(
- "Successfully initialized ContextDataFactory by retrieving the context data with {} entries",
- map.size());
if (pairs.size() == 1) {
final Iterator>> iter =
pairs.entrySet().iterator();
@@ -109,28 +98,20 @@ public Result filter(
private Result filter() {
boolean match = false;
if (useMap) {
- ReadOnlyStringMap currentContextData = null;
final IndexedReadOnlyStringMap map = getStringMap();
for (int i = 0; i < map.size(); i++) {
- if (currentContextData == null) {
- currentContextData = currentContextData();
- }
- final String toMatch = currentContextData.getValue(map.getKeyAt(i));
+ final String toMatch = ContextData.getValue(map.getKeyAt(i));
match = toMatch != null && ((List) map.getValueAt(i)).contains(toMatch);
if ((!isAnd() && match) || (isAnd() && !match)) {
break;
}
}
} else {
- match = value.equals(currentContextData().getValue(key));
+ match = value.equals(ContextData.getValue(key));
}
return match ? onMatch : onMismatch;
}
- private ReadOnlyStringMap currentContextData() {
- return injector.rawContextData();
- }
-
@Override
public Result filter(final LogEvent event) {
return super.filter(event.getContextData()) ? onMatch : onMismatch;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java
index 8ad77df4712..e84cfe76713 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/package-info.java
@@ -22,7 +22,7 @@
* {@link org.apache.logging.log4j.core.Filter#ELEMENT_TYPE filter}.
*/
@Export
-@Version("2.21.0")
+@Version("2.24.0")
package org.apache.logging.log4j.core.filter;
import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
index 141604810ca..aa73324e8bc 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextDataInjectorFactory.java
@@ -30,27 +30,25 @@
* Factory for ContextDataInjectors. Returns a new {@code ContextDataInjector} instance based on the value of system
* property {@code log4j2.ContextDataInjector}. Users may use this system property to specify the fully qualified class
* name of a class that implements the {@code ContextDataInjector} interface.
- * If no value was specified this factory method returns one of the injectors defined in
- * {@code ThreadContextDataInjector}.
*
* @see ContextDataInjector
* @see ReadOnlyStringMap
* @see ThreadContextDataInjector
* @see LogEvent#getContextData()
* @since 2.7
+ * @deprecated Use ContextDataProvider instead.
*/
+@Deprecated
public class ContextDataInjectorFactory {
private static final String CONTEXT_DATA_INJECTOR_PROPERTY = "log4j2.ContextDataInjector";
/**
* Returns a new {@code ContextDataInjector} instance based on the value of system property
- * {@code log4j2.ContextDataInjector}. If no value was specified this factory method returns one of the
- * {@code ContextDataInjector} classes defined in {@link ThreadContextDataInjector} which is most appropriate for
- * the ThreadContext implementation.
- *
+ * {@code log4j2.ContextDataInjector}. If no value was specified then @{link ContextData} will be used.
* Note: It is no longer recommended that users provide a custom implementation of the ContextDataInjector.
- * Instead, provide a {@code ContextDataProvider}.
+ * Instead, provide a {@code ContextDataProvider}. Support for ContextDataInjectors will be removed entirely
+ * in 3.0.0.
*
*
* Users may use this system property to specify the fully qualified class name of a class that implements the
@@ -65,16 +63,23 @@ public class ContextDataInjectorFactory {
* @return a ContextDataInjector that populates the {@code ReadOnlyStringMap} of all {@code LogEvent} objects
* @see LogEvent#getContextData()
* @see ContextDataInjector
+ * @deprecated Uses ContextData instead.
*/
+ @Deprecated
public static ContextDataInjector createInjector() {
+ return createInjector(false);
+ }
+
+ @Deprecated
+ public static ContextDataInjector createInjector(final boolean useDefault) {
try {
return LoaderUtil.newCheckedInstanceOfProperty(
CONTEXT_DATA_INJECTOR_PROPERTY,
ContextDataInjector.class,
- ContextDataInjectorFactory::createDefaultInjector);
+ useDefault ? ContextDataInjectorFactory::createDefaultInjector : () -> null);
} catch (final ReflectiveOperationException e) {
- StatusLogger.getLogger().warn("Could not create ContextDataInjector: {}", e.getMessage(), e);
- return createDefaultInjector();
+ StatusLogger.getLogger().info("Could not create ContextDataInjector: {}", e.getMessage(), e);
+ return null;
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
index 09c44413940..73853865e62 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/JdkMapAdapterStringMap.java
@@ -47,7 +47,7 @@ public class JdkMapAdapterStringMap implements StringMap {
// It is a cache, no need to synchronise it between threads.
private static Map, Void> UNMODIFIABLE_MAPS_CACHE = new WeakHashMap<>();
- private final Map map;
+ protected final Map map;
private boolean immutable = false;
private transient String[] sortedKeys;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
index 7185bc7bc8a..bc02b085bb9 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
@@ -24,6 +24,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
@@ -680,6 +681,11 @@ private static StringMap createContextData(final Map contextMap)
private static StringMap createContextData(final List properties) {
final StringMap reusable = ContextDataFactory.createContextData();
+ if (CONTEXT_DATA_INJECTOR == null) {
+ copyProperties(properties, reusable);
+ ContextData.addAll(reusable);
+ return reusable;
+ }
return CONTEXT_DATA_INJECTOR.injectContextData(properties, reusable);
}
@@ -979,6 +985,21 @@ public static Log4jLogEvent deserialize(final Serializable event) {
throw new IllegalArgumentException("Event is not a serialized LogEvent: " + event.toString());
}
+ /**
+ * Copies key-value pairs from the specified property list into the specified {@code StringMap}.
+ *
+ * @param properties list of configuration properties, may be {@code null}
+ * @param result the {@code StringMap} object to add the key-values to. Must be non-{@code null}.
+ */
+ private static void copyProperties(final List properties, final StringMap result) {
+ if (properties != null) {
+ for (int i = 0; i < properties.size(); i++) {
+ final Property prop = properties.get(i);
+ result.putValue(prop.getName(), prop.getValue());
+ }
+ }
+ }
+
private void readObject(final ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
index e954d9b7f71..5a916d40035 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
@@ -17,6 +17,7 @@
package org.apache.logging.log4j.core.impl;
import java.util.List;
+import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
@@ -100,7 +101,13 @@ public LogEvent createEvent(
result.initTime(CLOCK, Log4jLogEvent.getNanoClock());
result.setThrown(t);
result.setSource(location);
- result.setContextData(injector.injectContextData(properties, (StringMap) result.getContextData()));
+ if (injector != null) {
+ result.setContextData(injector.injectContextData(properties, (StringMap) result.getContextData()));
+ } else {
+ StringMap reusable = (StringMap) result.getContextData();
+ copyProperties(properties, reusable);
+ ContextData.addAll(reusable);
+ }
result.setContextStack(
ThreadContext.getDepth() == 0 ? ThreadContext.EMPTY_STACK : ThreadContext.cloneStack()); // mutable copy
@@ -111,6 +118,21 @@ public LogEvent createEvent(
return result;
}
+ /**
+ * Copies key-value pairs from the specified property list into the specified {@code StringMap}.
+ *
+ * @param properties list of configuration properties, may be {@code null}
+ * @param result the {@code StringMap} object to add the key-values to. Must be non-{@code null}.
+ */
+ private static void copyProperties(final List properties, final StringMap result) {
+ if (properties != null) {
+ for (int i = 0; i < properties.size(); i++) {
+ final Property prop = properties.get(i);
+ result.putValue(prop.getName(), prop.getValue());
+ }
+ }
+ }
+
private static MutableLogEvent getOrCreateMutableLogEvent() {
final MutableLogEvent result = mutableLogEventThreadLocal.get();
return result == null || result.reserved ? createInstance(result) : result;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
index 332b877a51f..839c3c2df53 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
@@ -16,17 +16,12 @@
*/
package org.apache.logging.log4j.core.impl;
-import aQute.bnd.annotation.Cardinality;
-import aQute.bnd.annotation.Resolution;
-import aQute.bnd.annotation.spi.ServiceConsumer;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.ServiceLoader;
-import java.util.concurrent.ConcurrentLinkedDeque;
+import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.ContextDataInjector;
@@ -35,7 +30,6 @@
import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
-import org.apache.logging.log4j.util.ServiceLoaderUtil;
import org.apache.logging.log4j.util.StringMap;
/**
@@ -43,7 +37,8 @@
* {@code ThreadContext} map implementations into a {@code StringMap}. In the case of duplicate keys,
* thread context values overwrite configuration {@code Property} values.
*
- * These are the default {@code ContextDataInjector} objects returned by the {@link ContextDataInjectorFactory}.
+ * This class is no longer directly used by Log4j. It is only present in case it is being overridden by a user.
+ * Will be removed in 3.0.0.
*
*
* @see org.apache.logging.log4j.ThreadContext
@@ -52,11 +47,9 @@
* @see ContextDataInjector
* @see ContextDataInjectorFactory
* @since 2.7
+ * @Deprecated Use @{link ContextData} instead.
*/
-@ServiceConsumer(
- value = ContextDataProvider.class,
- resolution = Resolution.OPTIONAL,
- cardinality = Cardinality.MULTIPLE)
+@Deprecated
public class ThreadContextDataInjector {
private static final Logger LOGGER = StatusLogger.getLogger();
@@ -64,9 +57,7 @@ public class ThreadContextDataInjector {
/**
* ContextDataProviders loaded via OSGi.
*/
- public static Collection contextDataProviders = new ConcurrentLinkedDeque<>();
-
- private static final List SERVICE_PROVIDERS = getServiceProviders();
+ public static Collection contextDataProviders = new ProviderQueue();
/**
* Previously this method allowed ContextDataProviders to be loaded eagerly, now they
@@ -77,16 +68,6 @@ public class ThreadContextDataInjector {
@Deprecated
public static void initServiceProviders() {}
- private static List getServiceProviders() {
- final List providers = new ArrayList<>();
- ServiceLoaderUtil.safeStream(
- ContextDataProvider.class,
- ServiceLoader.load(ContextDataProvider.class, ThreadContextDataInjector.class.getClassLoader()),
- LOGGER)
- .forEach(providers::add);
- return Collections.unmodifiableList(providers);
- }
-
/**
* Default {@code ContextDataInjector} for the legacy {@code Map}-based ThreadContext (which is
* also the ThreadContext implementation used for web applications).
@@ -95,11 +76,7 @@ private static List getServiceProviders() {
*/
public static class ForDefaultThreadContextMap implements ContextDataInjector {
- private final List providers;
-
- public ForDefaultThreadContextMap() {
- providers = getProviders();
- }
+ public ForDefaultThreadContextMap() {}
/**
* Puts key-value pairs from both the specified list of properties as well as the thread context into the
@@ -111,44 +88,12 @@ public ForDefaultThreadContextMap() {
*/
@Override
public StringMap injectContextData(final List props, final StringMap ignore) {
-
- final Map copy;
-
- if (providers.size() == 1) {
- copy = providers.get(0).supplyContextData();
- } else {
- copy = new HashMap<>();
- for (ContextDataProvider provider : providers) {
- copy.putAll(provider.supplyContextData());
- }
- }
-
- // The DefaultThreadContextMap stores context data in a Map.
- // This is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy.
- // If there are no configuration properties or providers returning a thin wrapper around the copy
- // is faster than copying the elements into the LogEvent's reusable StringMap.
- if ((props == null || props.isEmpty())) {
- // this will replace the LogEvent's context data with the returned instance.
- // NOTE: must mark as frozen or downstream components may attempt to modify (UnsupportedOperationEx)
- return copy.isEmpty() ? ContextDataFactory.emptyFrozenContextData() : frozenStringMap(copy);
- }
- // If the list of Properties is non-empty we need to combine the properties and the ThreadContext
- // data. Note that we cannot reuse the specified StringMap: some Loggers may have properties defined
- // and others not, so the LogEvent's context data may have been replaced with an immutable copy from
- // the ThreadContext - this will throw an UnsupportedOperationException if we try to modify it.
- final StringMap result = new JdkMapAdapterStringMap(new HashMap<>(copy), false);
- for (int i = 0; i < props.size(); i++) {
- final Property prop = props.get(i);
- if (!copy.containsKey(prop.getName())) {
- result.putValue(prop.getName(), prop.getValue());
- }
- }
- result.freeze();
- return result;
- }
-
- private static JdkMapAdapterStringMap frozenStringMap(final Map copy) {
- return new JdkMapAdapterStringMap(copy, true);
+ Map map = new HashMap<>();
+ JdkMapAdapterStringMap stringMap = new JdkMapAdapterStringMap(map, false);
+ copyProperties(props, stringMap);
+ ContextData.addAll(map);
+ stringMap.freeze();
+ return stringMap;
}
@Override
@@ -172,11 +117,8 @@ public ReadOnlyStringMap rawContextData() {
* This injector always puts key-value pairs into the specified reusable StringMap.
*/
public static class ForGarbageFreeThreadContextMap implements ContextDataInjector {
- private final List providers;
- public ForGarbageFreeThreadContextMap() {
- this.providers = getProviders();
- }
+ public ForGarbageFreeThreadContextMap() {}
/**
* Puts key-value pairs from both the specified list of properties as well as the thread context into the
@@ -192,12 +134,13 @@ public StringMap injectContextData(final List props, final StringMap r
// StringMap. We cannot return the ThreadContext's internal data structure because it may be modified later
// and such modifications should not be reflected in the log event.
copyProperties(props, reusable);
- for (int i = 0; i < providers.size(); ++i) {
- reusable.putAll(providers.get(i).supplyStringMap());
- }
+ ContextData.addAll(reusable);
return reusable;
}
+ /*
+ No longer used.
+ */
@Override
public ReadOnlyStringMap rawContextData() {
return ThreadContext.getThreadContextMap().getReadOnlyContextData();
@@ -205,59 +148,9 @@ public ReadOnlyStringMap rawContextData() {
}
/**
- * The {@code ContextDataInjector} used when the ThreadContextMap implementation is a copy-on-write
- * StringMap-based data structure.
- *
- * If there are no configuration properties, this injector will return the thread context's internal data
- * structure. Otherwise the configuration properties are combined with the thread context key-value pairs into the
- * specified reusable StringMap.
+ * Th
*/
- public static class ForCopyOnWriteThreadContextMap implements ContextDataInjector {
- private final List providers;
-
- public ForCopyOnWriteThreadContextMap() {
- this.providers = getProviders();
- }
- /**
- * If there are no configuration properties, this injector will return the thread context's internal data
- * structure. Otherwise the configuration properties are combined with the thread context key-value pairs into the
- * specified reusable StringMap.
- *
- * @param props list of configuration properties, may be {@code null}
- * @param ignore a {@code StringMap} instance from the log event
- * @return a {@code StringMap} combining configuration properties with thread context data
- */
- @Override
- public StringMap injectContextData(final List props, final StringMap ignore) {
- // If there are no configuration properties we want to just return the ThreadContext's StringMap:
- // it is a copy-on-write data structure so we are sure ThreadContext changes will not affect our copy.
- if (providers.size() == 1 && (props == null || props.isEmpty())) {
- // this will replace the LogEvent's context data with the returned instance
- return providers.get(0).supplyStringMap();
- }
- int count = props == null ? 0 : props.size();
- final StringMap[] maps = new StringMap[providers.size()];
- for (int i = 0; i < providers.size(); ++i) {
- maps[i] = providers.get(i).supplyStringMap();
- count += maps[i].size();
- }
- // However, if the list of Properties is non-empty we need to combine the properties and the ThreadContext
- // data. Note that we cannot reuse the specified StringMap: some Loggers may have properties defined
- // and others not, so the LogEvent's context data may have been replaced with an immutable copy from
- // the ThreadContext - this will throw an UnsupportedOperationException if we try to modify it.
- final StringMap result = ContextDataFactory.createContextData(count);
- copyProperties(props, result);
- for (StringMap map : maps) {
- result.putAll(map);
- }
- return result;
- }
-
- @Override
- public ReadOnlyStringMap rawContextData() {
- return ThreadContext.getThreadContextMap().getReadOnlyContextData();
- }
- }
+ public static class ForCopyOnWriteThreadContextMap extends ForDefaultThreadContextMap {}
/**
* Copies key-value pairs from the specified property list into the specified {@code StringMap}.
@@ -274,11 +167,134 @@ public static void copyProperties(final List properties, final StringM
}
}
- private static List getProviders() {
- final List providers =
- new ArrayList<>(contextDataProviders.size() + SERVICE_PROVIDERS.size());
- providers.addAll(contextDataProviders);
- providers.addAll(SERVICE_PROVIDERS);
- return providers;
+ private static class ProviderQueue implements Collection {
+ @Override
+ public int size() {
+ return ContextData.contextDataProviders.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return ContextData.contextDataProviders.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return ContextData.contextDataProviders.contains(o);
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new ProviderIterator(ContextData.contextDataProviders.iterator());
+ }
+
+ @Override
+ public Object[] toArray() {
+ return ContextData.contextDataProviders.toArray();
+ }
+
+ @Override
+ public T[] toArray(T[] a) {
+ return ContextData.contextDataProviders.toArray(a);
+ }
+
+ @Override
+ public boolean add(ContextDataProvider contextDataProvider) {
+ return ContextData.contextDataProviders.add(contextDataProvider);
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ return ContextData.contextDataProviders.remove(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection> c) {
+ return ContextData.contextDataProviders.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection extends ContextDataProvider> c) {
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection> c) {
+ return ContextData.contextDataProviders.removeAll(c);
+ }
+
+ @Override
+ public boolean retainAll(Collection> c) {
+ return ContextData.contextDataProviders.retainAll(c);
+ }
+
+ @Override
+ public void clear() {
+ ContextData.contextDataProviders.clear();
+ }
+ }
+
+ private static class ProviderIterator implements Iterator {
+
+ private final Iterator iter;
+
+ public ProviderIterator(Iterator iter) {
+ this.iter = iter;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ @Override
+ public ContextDataProvider next() {
+ org.apache.logging.log4j.spi.ContextDataProvider next = iter.next();
+ if (next instanceof ContextDataProvider) {
+ return (ContextDataProvider) next;
+ } else if (next != null) {
+ return new ProviderWrapper(next);
+ }
+ return null;
+ }
+ }
+
+ private static class ProviderWrapper implements ContextDataProvider {
+
+ private final org.apache.logging.log4j.spi.ContextDataProvider provider;
+
+ public ProviderWrapper(org.apache.logging.log4j.spi.ContextDataProvider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ public String get(String key) {
+ return provider.get(key);
+ }
+
+ @Override
+ public int size() {
+ return provider.size();
+ }
+
+ @Override
+ public void addAll(Map map) {
+ provider.addAll(map);
+ }
+
+ @Override
+ public void addAll(StringMap map) {
+ provider.addAll(map);
+ }
+
+ @Override
+ public Map supplyContextData() {
+ return provider.supplyContextData();
+ }
+
+ @Override
+ public StringMap supplyStringMap() {
+ return new JdkMapAdapterStringMap(supplyContextData(), true);
+ }
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
index 86778d2bd7f..b24f1203a8c 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
@@ -16,17 +16,18 @@
*/
package org.apache.logging.log4j.core.lookup;
+import org.apache.logging.log4j.ContextData;
+import org.apache.logging.log4j.ScopedContext;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.ContextDataInjector;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
-import org.apache.logging.log4j.util.ReadOnlyStringMap;
/**
- * Looks up keys from the context. By default this is the {@link ThreadContext}, but users may
- * {@linkplain ContextDataInjectorFactory configure} a custom {@link ContextDataInjector} which obtains context data
- * from some other source.
+ * Looks up keys from the context. By default this is the {@link ThreadContext} or {@link ScopedContext}. Users may
+ * add their own {@link org.apache.logging.log4j.core.util.ContextDataProvider} which can be retrieved via this
+ * Lookup.
*/
@Plugin(name = "ctx", category = StrLookup.CATEGORY)
public class ContextMapLookup implements StrLookup {
@@ -40,11 +41,10 @@ public class ContextMapLookup implements StrLookup {
*/
@Override
public String lookup(final String key) {
- return currentContextData().getValue(key);
- }
-
- private ReadOnlyStringMap currentContextData() {
- return injector.rawContextData();
+ if (injector == null) {
+ return ContextData.getValue(key);
+ }
+ return injector.getValue(key);
}
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java
index 12526349ce2..d07f21e49e4 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/package-info.java
@@ -21,7 +21,7 @@
* {@link org.apache.logging.log4j.core.lookup.StrLookup#CATEGORY Lookup}.
*/
@Export
-@Version("2.20.1")
+@Version("2.24.0")
package org.apache.logging.log4j.core.lookup;
import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
index 4fae66de228..17bde215fff 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
@@ -18,14 +18,14 @@
import java.util.Collection;
import java.util.concurrent.atomic.AtomicReference;
+import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
import org.apache.logging.log4j.core.impl.Log4jProvider;
-import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
-import org.apache.logging.log4j.core.impl.ThreadContextDataProvider;
import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.core.util.ContextDataProvider;
+import org.apache.logging.log4j.spi.ContextDataProvider;
+import org.apache.logging.log4j.spi.ThreadContextDataProvider;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.apache.logging.log4j.util.ProviderActivator;
@@ -99,7 +99,7 @@ private static void loadContextProviders(final BundleContext bundleContext) {
bundleContext.getServiceReferences(ContextDataProvider.class, null);
for (final ServiceReference serviceReference : serviceReferences) {
final ContextDataProvider provider = bundleContext.getService(serviceReference);
- ThreadContextDataInjector.contextDataProviders.add(provider);
+ ContextData.addProvider(provider);
}
} catch (final InvalidSyntaxException ex) {
LOGGER.error("Error accessing context data provider", ex);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java
index a11875eea41..266256b4637 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/package-info.java
@@ -18,7 +18,7 @@
* Implementation of Log4j 2.
*/
@Export
-@Version("2.20.2")
+@Version("2.24.0")
package org.apache.logging.log4j.core;
import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
index 8ac63b6858d..086ac93981d 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
@@ -22,17 +22,11 @@
/**
* Source of context data to be added to each log event.
+ * @deprecated Use ContextDataProvider from Log4j API from 2.24.0.
*/
-public interface ContextDataProvider {
+@Deprecated
+public interface ContextDataProvider extends org.apache.logging.log4j.spi.ContextDataProvider {
- /**
- * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
- *
- * Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
- * underlying context data will not be reflected in the returned object.
- *
- * @return A Map containing the context data or null.
- */
Map supplyContextData();
/**
@@ -42,7 +36,9 @@ public interface ContextDataProvider {
* underlying context data will not be reflected in the returned object.
*
* @return the context data in a StringMap.
+ * @deprecated No longer used since 2.24.0. Will be removed in 3.0.0.
*/
+ @Deprecated
default StringMap supplyStringMap() {
return new JdkMapAdapterStringMap(supplyContextData(), true);
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java
index 753b7c456bd..6c602546054 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/package-info.java
@@ -18,7 +18,7 @@
* Log4j 2 helper classes.
*/
@Export
-@Version("2.20.2")
+@Version("2.24.0")
package org.apache.logging.log4j.core.util;
import org.osgi.annotation.bundle.Export;
From d398dae00952dd38d2d378d20480d8e87324139b Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Wed, 3 Apr 2024 08:54:23 -0700
Subject: [PATCH 05/19] Update docs
---
src/changelog/.2.x.x/add_scoped_context.xml | 2 +-
src/site/antora/modules/ROOT/pages/manual/extending.adoc | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/changelog/.2.x.x/add_scoped_context.xml b/src/changelog/.2.x.x/add_scoped_context.xml
index 06db3eb0d54..fb74c221114 100644
--- a/src/changelog/.2.x.x/add_scoped_context.xml
+++ b/src/changelog/.2.x.x/add_scoped_context.xml
@@ -5,5 +5,5 @@
type="updated">
- Add ScopedContext to log4j-api and ScopedContextDataProvider in log4j-core.
+ Add ScopedContext to log4j-api and ScopedContextDataProvider in log4j-core. Moved ContextDataProvider to log4j-api to allow custom data providers to be included in lookups.
diff --git a/src/site/antora/modules/ROOT/pages/manual/extending.adoc b/src/site/antora/modules/ROOT/pages/manual/extending.adoc
index aa1beb0fbf1..f777ab2c19f 100644
--- a/src/site/antora/modules/ROOT/pages/manual/extending.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/extending.adoc
@@ -586,8 +586,7 @@ ListAppender list2 = ListAppender.newBuilder().setName("List1").setEntryPerNewLi
The link:../javadoc/log4j-core/org/apache/logging/log4j/core/util/ContextDataProvider.html[`ContextDataProvider`]
(introduced in Log4j 2.13.2) is an interface applications and libraries can use to inject
-additional key-value pairs into the LogEvent's context data. Log4j's
-link:../javadoc/log4j-core/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.html[`ThreadContextDataInjector`]
+additional key-value pairs into the LogEvent's context data. Log4j
uses `java.util.ServiceLoader` to locate and load `ContextDataProvider` instances.
Log4j itself adds the ThreadContext data to the LogEvent using
`org.apache.logging.log4j.core.impl.ThreadContextDataProvider`. Custom implementations
From 37e2b224e1e919ea708ad2a6d0442b0c8a59f1a2 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Wed, 3 Apr 2024 09:13:51 -0700
Subject: [PATCH 06/19] Remove ParameterizedMapMessage
---
.../message/ParameterizedMapMessage.java | 38 ---
.../ParameterizedMapMessageFactory.java | 216 ------------------
2 files changed, 254 deletions(-)
delete mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessage.java
delete mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessageFactory.java
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
deleted file mode 100644
index 292bbb8290b..00000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessage.java
+++ /dev/null
@@ -1,38 +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 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 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
deleted file mode 100644
index 48575c08499..00000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMapMessageFactory.java
+++ /dev/null
@@ -1,216 +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 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> mapSupplier;
-
- public ParameterizedMapMessageFactory(Supplier> mapSupplier) {
- this.mapSupplier = mapSupplier;
- }
-
- @Override
- public Message newMessage(final CharSequence message) {
- Map map = mapSupplier.get();
- Message msg = new SimpleMessage(message);
- return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
- }
-
- @Override
- public Message newMessage(final Object message) {
- Map map = mapSupplier.get();
- Message msg = new ObjectMessage(message);
- return map.isEmpty() ? msg : new ParameterizedMapMessage(msg, map);
- }
-
- @Override
- public Message newMessage(final String message) {
- Map 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 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 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 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 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 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 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 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 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 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 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 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);
- }
-}
From 4848144695791a63d3f105613905cfe514f85798 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Wed, 3 Apr 2024 14:21:10 -0700
Subject: [PATCH 07/19] Remove ParameterizedMapMessage from test
---
.../main/java/org/apache/logging/log4j/test/TestLogger.java | 6 ------
1 file changed, 6 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 2387fee7e6a..b308434cfc9 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
@@ -28,7 +28,6 @@
import org.apache.logging.log4j.Marker;
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;
/**
@@ -88,11 +87,6 @@ protected void log(
sb.append(mdc);
sb.append(' ');
}
- if (message instanceof ParameterizedMapMessage) {
- sb.append(" Map data: ");
- sb.append(((ParameterizedMapMessage) message).getData().toString());
- sb.append(' ');
- }
final Object[] params = message.getParameters();
final Throwable t;
if (throwable == null
From 7f63f315e468b1d78e45e6922445bbdd57fd32d5 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Wed, 3 Apr 2024 15:09:55 -0700
Subject: [PATCH 08/19] Fix typo
---
.../logging/log4j/core/impl/ThreadContextDataInjector.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
index 839c3c2df53..0362984c5b2 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
@@ -47,7 +47,7 @@
* @see ContextDataInjector
* @see ContextDataInjectorFactory
* @since 2.7
- * @Deprecated Use @{link ContextData} instead.
+ * @deprecated Use @{link ContextData} instead.
*/
@Deprecated
public class ThreadContextDataInjector {
From b26c2e278143b7e967a01e5b7414419dbb6de9be Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Wed, 3 Apr 2024 21:07:28 -0700
Subject: [PATCH 09/19] Ensure unit test uses a unique file name
---
.../log4j/core/appender/routing/JsonRoutingAppender2Test.java | 2 +-
log4j-core-test/src/test/resources/log4j-routing2.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/routing/JsonRoutingAppender2Test.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/routing/JsonRoutingAppender2Test.java
index 2b53e9e6ce7..55dea07c5d9 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/routing/JsonRoutingAppender2Test.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/routing/JsonRoutingAppender2Test.java
@@ -34,7 +34,7 @@
*/
public class JsonRoutingAppender2Test {
private static final String CONFIG = "log4j-routing2.json";
- private static final String LOG_FILENAME = "target/rolling1/rollingtest-Unknown.log";
+ private static final String LOG_FILENAME = "target/rolling1/routingtest2-Unknown.log";
private final LoggerContextRule loggerContextRule = new LoggerContextRule(CONFIG);
diff --git a/log4j-core-test/src/test/resources/log4j-routing2.json b/log4j-core-test/src/test/resources/log4j-routing2.json
index baf475ad077..b98451450ba 100644
--- a/log4j-core-test/src/test/resources/log4j-routing2.json
+++ b/log4j-core-test/src/test/resources/log4j-routing2.json
@@ -16,7 +16,7 @@
*/
{ "configuration": { "status": "error", "name": "RoutingTest",
"properties": {
- "property": { "name": "filename", "value" : "target/rolling1/rollingtest-$${sd:type}.log" }
+ "property": { "name": "filename", "value" : "target/rolling1/routingtest2-$${sd:type}.log" }
},
"ThresholdFilter": { "level": "debug" },
"appenders": {
From 9ca817afac6e158d45790b9727680b9747826aa3 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Thu, 4 Apr 2024 08:14:49 -0700
Subject: [PATCH 10/19] Move non-API classes to core
---
.../apache/logging/log4j/test/TestLogger.java | 10 +-
.../apache/logging/log4j/ScopedContext.java | 123 +++++++++++-------
.../logging/log4j/simple/SimpleLogger.java | 7 +-
.../log4j/spi/ContextDataProvider.java | 76 -----------
.../log4j/spi/ScopedContextDataProvider.java | 104 ---------------
.../logging/log4j/core/async/AsyncLogger.java | 2 +-
.../async/RingBufferLogEventTranslator.java | 2 +-
.../core/filter/DynamicThresholdFilter.java | 4 +-
.../core/filter/ThreadContextMapFilter.java | 2 +-
.../logging/log4j/core/impl}/ContextData.java | 5 +-
.../log4j/core/impl/Log4jLogEvent.java | 1 -
.../core/impl/ReusableLogEventFactory.java | 1 -
.../core/impl/ScopedContextDataProvider.java | 27 ++--
.../core/impl/ThreadContextDataInjector.java | 11 +-
.../core/impl/ThreadContextDataProvider.java | 15 +++
.../log4j/core/lookup/ContextMapLookup.java | 2 +-
.../logging/log4j/core/osgi/Activator.java | 6 +-
.../log4j/core/util/ContextDataProvider.java | 52 +++++++-
18 files changed, 190 insertions(+), 260 deletions(-)
delete mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
delete mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
rename {log4j-api/src/main/java/org/apache/logging/log4j => log4j-core/src/main/java/org/apache/logging/log4j/core/impl}/ContextData.java (96%)
rename log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java => log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java (60%)
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 b308434cfc9..5f1fb02de7b 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
@@ -23,9 +23,10 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.apache.logging.log4j.ContextData;
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;
import org.apache.logging.log4j.spi.AbstractLogger;
@@ -80,8 +81,11 @@ protected void log(
sb.append(' ');
}
sb.append(message.getFormattedMessage());
- final Map mdc = new HashMap<>(ContextData.size());
- ContextData.addAll(mdc);
+ Map contextMap = ScopedContext.getContextMap();
+ final Map mdc = new HashMap<>(ThreadContext.getImmutableContext());
+ if (contextMap != null && !contextMap.isEmpty()) {
+ contextMap.forEach((key, value) -> mdc.put(key, value.render()));
+ }
if (!mdc.isEmpty()) {
sb.append(' ');
sb.append(mdc);
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
index 67e2ba97fde..4081be5629b 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
@@ -16,7 +16,9 @@
*/
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.Optional;
@@ -25,7 +27,6 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Supplier;
-import org.apache.logging.log4j.spi.ScopedContextDataProvider;
import org.apache.logging.log4j.status.StatusLogger;
/**
@@ -52,6 +53,48 @@ public class ScopedContext {
public static final Logger LOGGER = StatusLogger.getLogger();
+ private static final ThreadLocal> 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.
+ */
+ private static Optional getContext() {
+ Deque stack = scopedContext.get();
+ if (stack != null) {
+ return Optional.of(stack.getFirst());
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Add the ScopeContext.
+ * @param context The ScopeContext.
+ */
+ private static void addScopedContext(Instance context) {
+ Deque stack = scopedContext.get();
+ if (stack == null) {
+ stack = new ArrayDeque<>();
+ scopedContext.set(stack);
+ }
+ stack.addFirst(context);
+ }
+
+ /**
+ * Remove the top ScopeContext.
+ */
+ private static void removeScopedContext() {
+ Deque stack = scopedContext.get();
+ if (stack != null) {
+ if (!stack.isEmpty()) {
+ stack.removeFirst();
+ }
+ if (stack.isEmpty()) {
+ scopedContext.remove();
+ }
+ }
+ }
+
/**
* @hidden
* Returns an unmodifiable copy of the current ScopedContext Map. This method should
@@ -59,7 +102,7 @@ public class ScopedContext {
* @return the Map of Renderable objects.
*/
public static Map getContextMap() {
- Optional context = ScopedContextDataProvider.getContext();
+ Optional context = getContext();
if (context.isPresent()
&& context.get().contextMap != null
&& !context.get().contextMap.isEmpty()) {
@@ -74,7 +117,7 @@ public static Map getContextMap() {
* @return the number of items in the context map.
*/
public static int size() {
- Optional context = ScopedContextDataProvider.getContext();
+ Optional context = getContext();
return context.map(instance -> instance.contextMap.size()).orElse(0);
}
@@ -85,7 +128,7 @@ public static int size() {
*/
@SuppressWarnings("unchecked")
public static T get(String key) {
- Optional context = ScopedContextDataProvider.getContext();
+ Optional context = getContext();
if (context.isPresent()) {
Renderable renderable = context.get().contextMap.get(key);
if (renderable != null) {
@@ -101,7 +144,7 @@ public static T get(String key) {
* @return The value of the key in the current ScopedContext.
*/
public static String getString(String key) {
- Optional context = ScopedContextDataProvider.getContext();
+ Optional context = getContext();
if (context.isPresent()) {
Renderable renderable = context.get().contextMap.get(key);
if (renderable != null) {
@@ -116,7 +159,7 @@ public static String getString(String key) {
* @param map The Map to add entries to.
*/
public static void addAll(Map map) {
- Optional context = ScopedContextDataProvider.getContext();
+ Optional context = getContext();
if (context.isPresent()) {
Map contextMap = context.get().contextMap;
if (contextMap != null && !contextMap.isEmpty()) {
@@ -136,16 +179,16 @@ public static void addAll(Map map) {
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;
+ Instance parent = getContext().isPresent() ? getContext().get() : null;
return new Instance(parent, key, renderable);
} else {
- if (current().isPresent()) {
+ if (getContext().isPresent()) {
Map map = getContextMap();
map.remove(key);
return new Instance(map);
}
}
- return current().isPresent() ? current().get() : new Instance();
+ return getContext().isPresent() ? getContext().get() : new Instance();
}
/**
@@ -167,8 +210,8 @@ public static Instance where(String key, Supplier supplier) {
public static Instance where(Map map) {
if (map != null && !map.isEmpty()) {
Map renderableMap = new HashMap<>();
- if (current().isPresent()) {
- renderableMap.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ renderableMap.putAll(getContext().get().contextMap);
}
map.forEach((key, value) -> {
if (value == null || (value instanceof String && ((String) value).isEmpty())) {
@@ -180,7 +223,7 @@ public static Instance where(Map map) {
});
return new Instance(renderableMap);
} else {
- return current().isPresent() ? current().get() : new Instance();
+ return getContext().isPresent() ? getContext().get() : new Instance();
}
}
@@ -194,15 +237,15 @@ public static void runWhere(String key, Object obj, Runnable op) {
if (obj != null) {
Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.put(key, renderable);
new Instance(map).run(op);
} else {
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.remove(key);
new Instance(map).run(op);
@@ -220,8 +263,8 @@ public static Future> runWhere(String key, Object obj, ExecutorService executo
if (obj != null) {
Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.put(key, renderable);
if (executorService != null) {
@@ -233,8 +276,8 @@ public static Future> runWhere(String key, Object obj, ExecutorService executo
}
} else {
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.remove(key);
if (executorService != null) {
@@ -255,8 +298,8 @@ public static Future> runWhere(String key, Object obj, ExecutorService executo
public static void runWhere(Map map, Runnable op) {
if (map != null && !map.isEmpty()) {
Map renderableMap = new HashMap<>();
- if (current().isPresent()) {
- renderableMap.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ renderableMap.putAll(getContext().get().contextMap);
}
map.forEach((key, value) -> {
renderableMap.put(key, value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value));
@@ -277,15 +320,15 @@ public static R callWhere(String key, Object obj, Callable op) throws Exc
if (obj != null) {
Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.put(key, renderable);
return new Instance(map).call(op);
} else {
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.remove(key);
return new Instance(map).call(op);
@@ -304,8 +347,8 @@ public static Future callWhere(String key, Object obj, ExecutorService ex
if (obj != null) {
Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.put(key, renderable);
if (executorService != null) {
@@ -318,8 +361,8 @@ public static Future callWhere(String key, Object obj, ExecutorService ex
} else {
if (executorService != null) {
Map map = new HashMap<>();
- if (current().isPresent()) {
- map.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ map.putAll(getContext().get().contextMap);
}
map.remove(key);
return executorService.submit(new Caller(
@@ -339,8 +382,8 @@ public static Future callWhere(String key, Object obj, ExecutorService ex
public static R callWhere(Map map, Callable op) throws Exception {
if (map != null && !map.isEmpty()) {
Map renderableMap = new HashMap<>();
- if (current().isPresent()) {
- renderableMap.putAll(current().get().contextMap);
+ if (getContext().isPresent()) {
+ renderableMap.putAll(getContext().get().contextMap);
}
map.forEach((key, value) -> {
renderableMap.put(key, value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value));
@@ -351,14 +394,6 @@ public static R callWhere(Map map, Callable op) throws Excepti
}
}
- /**
- * Returns an Optional holding the active ScopedContext.Instance
- * @return an Optional containing the active ScopedContext, if there is one.
- */
- private static Optional current() {
- return ScopedContextDataProvider.getContext();
- }
-
public static class Instance {
private final Instance parent;
@@ -500,11 +535,11 @@ public void run() {
if (contextStack != null) {
ThreadContext.setStack(contextStack);
}
- ScopedContextDataProvider.addScopedContext(scopedContext);
+ addScopedContext(scopedContext);
try {
op.run();
} finally {
- ScopedContextDataProvider.removeScopedContext();
+ removeScopedContext();
ThreadContext.clearAll();
}
}
@@ -551,11 +586,11 @@ public R call() throws Exception {
if (contextStack != null) {
ThreadContext.setStack(contextStack);
}
- ScopedContextDataProvider.addScopedContext(scopedContext);
+ addScopedContext(scopedContext);
try {
return op.call();
} finally {
- ScopedContextDataProvider.removeScopedContext();
+ removeScopedContext();
ThreadContext.clearAll();
}
}
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 d914f1c4c93..f5529f4258d 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
@@ -23,9 +23,10 @@
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
-import org.apache.logging.log4j.ContextData;
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;
import org.apache.logging.log4j.spi.AbstractLogger;
@@ -295,8 +296,8 @@ public void logMessage(
}
sb.append(msg.getFormattedMessage());
if (showContextMap) {
- final Map mdc = new HashMap<>(ContextData.size());
- ContextData.addAll(mdc);
+ final Map 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());
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
deleted file mode 100644
index d003e9c74f1..00000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ContextDataProvider.java
+++ /dev/null
@@ -1,76 +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.spi;
-
-import java.util.Map;
-import org.apache.logging.log4j.util.StringMap;
-
-/**
- * Source of context data to be added to each log event.
- */
-public interface ContextDataProvider {
-
- /**
- * Returns the key for a value from the context data.
- * @param key the key to locate.
- * @return the value or null if it is not found.
- */
- default String get(String key) {
- return null;
- }
-
- /**
- * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
- *
- * Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
- * underlying context data will not be reflected in the returned object.
- *
- * @return A Map containing the context data or null.
- */
- Map supplyContextData();
-
- /**
- * Returns the number of items in this context.
- * @return the number of items in the context.
- */
- default int size() {
- Map contextMap = supplyContextData();
- return contextMap != null ? contextMap.size() : 0;
- }
-
- /**
- * Add all the keys in the current context to the provided Map.
- * @param map the StringMap to add the keys and values to.
- */
- default void addAll(Map map) {
- Map contextMap = supplyContextData();
- if (contextMap != null) {
- map.putAll(contextMap);
- }
- }
-
- /**
- * Add all the keys in the current context to the provided StringMap.
- * @param map the StringMap to add the keys and values to.
- */
- default void addAll(StringMap map) {
- Map contextMap = supplyContextData();
- if (contextMap != null) {
- contextMap.forEach(map::putValue);
- }
- }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
deleted file mode 100644
index f2a14c19d0b..00000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextDataProvider.java
+++ /dev/null
@@ -1,104 +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.spi;
-
-import aQute.bnd.annotation.Resolution;
-import aQute.bnd.annotation.spi.ServiceProvider;
-import java.util.ArrayDeque;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import org.apache.logging.log4j.ScopedContext;
-
-/**
- * ContextDataProvider for {@code Map} data.
- * @since 2.24.0
- */
-@ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
-public class ScopedContextDataProvider implements ContextDataProvider {
-
- private static final ThreadLocal> 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 getContext() {
- Deque 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 stack = scopedContext.get();
- if (stack == null) {
- stack = new ArrayDeque<>();
- scopedContext.set(stack);
- }
- stack.addFirst(context);
- }
-
- /**
- * Remove the top ScopeContext.
- */
- public static void removeScopedContext() {
- Deque stack = scopedContext.get();
- if (stack != null) {
- if (!stack.isEmpty()) {
- stack.removeFirst();
- }
- if (stack.isEmpty()) {
- scopedContext.remove();
- }
- }
- }
-
- @Override
- public String get(String key) {
- return ScopedContext.getString(key);
- }
-
- @Override
- public Map supplyContextData() {
- Map contextMap = ScopedContext.getContextMap();
- if (!contextMap.isEmpty()) {
- Map map = new HashMap<>();
- contextMap.forEach((key, value) -> map.put(key, value.render()));
- return map;
- } else {
- return Collections.emptyMap();
- }
- }
-
- @Override
- public int size() {
- return ScopedContext.size();
- }
-
- @Override
- public void addAll(Map map) {
- ScopedContext.addAll(map);
- }
-}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
index 5bb479f66ce..f8b5b114026 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLogger.java
@@ -19,7 +19,6 @@
import com.lmax.disruptor.EventTranslatorVararg;
import com.lmax.disruptor.dsl.Disruptor;
import java.util.List;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
@@ -31,6 +30,7 @@
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.config.ReliabilityStrategy;
+import org.apache.logging.log4j.core.impl.ContextData;
import org.apache.logging.log4j.core.impl.ContextDataFactory;
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
import org.apache.logging.log4j.core.util.Clock;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
index c965ce974d6..ef79ca5b3ec 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
@@ -17,11 +17,11 @@
package org.apache.logging.log4j.core.async;
import com.lmax.disruptor.EventTranslator;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext.ContextStack;
import org.apache.logging.log4j.core.ContextDataInjector;
+import org.apache.logging.log4j.core.impl.ContextData;
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
import org.apache.logging.log4j.core.util.Clock;
import org.apache.logging.log4j.core.util.NanoClock;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
index d2de85fe4da..b1f3c8f0d25 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/DynamicThresholdFilter.java
@@ -19,7 +19,6 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
@@ -32,11 +31,12 @@
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.core.impl.ContextData;
import org.apache.logging.log4j.core.impl.ContextDataFactory;
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.message.Message;
-import org.apache.logging.log4j.spi.ContextDataProvider;
import org.apache.logging.log4j.util.PerformanceSensitive;
import org.apache.logging.log4j.util.StringMap;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
index 7b27f884501..51168765559 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/ThreadContextMapFilter.java
@@ -21,7 +21,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.Filter;
@@ -33,6 +32,7 @@
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+import org.apache.logging.log4j.core.impl.ContextData;
import org.apache.logging.log4j.core.util.KeyValuePair;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextData.java
similarity index 96%
rename from log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextData.java
index 1b9445dcd80..df951a2e9fd 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ContextData.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ContextData.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j;
+package org.apache.logging.log4j.core.impl;
import java.util.ArrayList;
import java.util.Collection;
@@ -24,7 +24,8 @@
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
-import org.apache.logging.log4j.spi.ContextDataProvider;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.ServiceLoaderUtil;
import org.apache.logging.log4j.util.StringMap;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
index bc02b085bb9..aff4d5893c3 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jLogEvent.java
@@ -24,7 +24,6 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
index 5a916d40035..5b41469aacd 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
@@ -17,7 +17,6 @@
package org.apache.logging.log4j.core.impl;
import java.util.List;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
similarity index 60%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
index d6e28f00d85..805c0979e7c 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ThreadContextDataProvider.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ScopedContextDataProvider.java
@@ -14,36 +14,47 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.core.impl;
import aQute.bnd.annotation.Resolution;
import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
-import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.ScopedContext;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
/**
- * ContextDataProvider for ThreadContext data.
+ * ContextDataProvider for {@code Map} data.
+ * @since 2.24.0
*/
@ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
-public class ThreadContextDataProvider implements ContextDataProvider {
+public class ScopedContextDataProvider implements ContextDataProvider {
@Override
public String get(String key) {
- return ThreadContext.get(key);
+ return ScopedContext.getString(key);
}
@Override
public Map supplyContextData() {
- return ThreadContext.getImmutableContext();
+ Map contextMap = ScopedContext.getContextMap();
+ if (!contextMap.isEmpty()) {
+ Map map = new HashMap<>();
+ contextMap.forEach((key, value) -> map.put(key, value.render()));
+ return map;
+ } else {
+ return Collections.emptyMap();
+ }
}
@Override
public int size() {
- return ThreadContext.getContext().size();
+ return ScopedContext.size();
}
@Override
public void addAll(Map map) {
- map.putAll(ThreadContext.getContext());
+ ScopedContext.addAll(map);
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
index 0362984c5b2..06b9bcd0844 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
@@ -21,7 +21,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.ContextDataInjector;
@@ -236,9 +235,9 @@ public void clear() {
private static class ProviderIterator implements Iterator {
- private final Iterator iter;
+ private final Iterator iter;
- public ProviderIterator(Iterator iter) {
+ public ProviderIterator(Iterator iter) {
this.iter = iter;
}
@@ -249,7 +248,7 @@ public boolean hasNext() {
@Override
public ContextDataProvider next() {
- org.apache.logging.log4j.spi.ContextDataProvider next = iter.next();
+ ContextDataProvider next = iter.next();
if (next instanceof ContextDataProvider) {
return (ContextDataProvider) next;
} else if (next != null) {
@@ -261,9 +260,9 @@ public ContextDataProvider next() {
private static class ProviderWrapper implements ContextDataProvider {
- private final org.apache.logging.log4j.spi.ContextDataProvider provider;
+ private final ContextDataProvider provider;
- public ProviderWrapper(org.apache.logging.log4j.spi.ContextDataProvider provider) {
+ public ProviderWrapper(ContextDataProvider provider) {
this.provider = provider;
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java
index a20216c7559..949f8868a45 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataProvider.java
@@ -29,6 +29,11 @@
@ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL)
public class ThreadContextDataProvider implements ContextDataProvider {
+ @Override
+ public String get(String key) {
+ return ThreadContext.get(key);
+ }
+
@Override
public Map supplyContextData() {
return ThreadContext.getImmutableContext();
@@ -38,4 +43,14 @@ public Map supplyContextData() {
public StringMap supplyStringMap() {
return ThreadContext.getThreadContextMap().getReadOnlyContextData();
}
+
+ @Override
+ public int size() {
+ return ThreadContext.getContext().size();
+ }
+
+ @Override
+ public void addAll(Map map) {
+ map.putAll(ThreadContext.getContext());
+ }
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
index b24f1203a8c..9b841bb3961 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/lookup/ContextMapLookup.java
@@ -16,12 +16,12 @@
*/
package org.apache.logging.log4j.core.lookup;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.ScopedContext;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.ContextDataInjector;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.impl.ContextData;
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
index 17bde215fff..5f6f1da67c6 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/osgi/Activator.java
@@ -18,14 +18,14 @@
import java.util.Collection;
import java.util.concurrent.atomic.AtomicReference;
-import org.apache.logging.log4j.ContextData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
+import org.apache.logging.log4j.core.impl.ContextData;
import org.apache.logging.log4j.core.impl.Log4jProvider;
+import org.apache.logging.log4j.core.impl.ThreadContextDataProvider;
import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.spi.ContextDataProvider;
-import org.apache.logging.log4j.spi.ThreadContextDataProvider;
+import org.apache.logging.log4j.core.util.ContextDataProvider;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.apache.logging.log4j.util.ProviderActivator;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
index 086ac93981d..6eeeeef9b50 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ContextDataProvider.java
@@ -22,11 +22,26 @@
/**
* Source of context data to be added to each log event.
- * @deprecated Use ContextDataProvider from Log4j API from 2.24.0.
*/
-@Deprecated
-public interface ContextDataProvider extends org.apache.logging.log4j.spi.ContextDataProvider {
+public interface ContextDataProvider {
+ /**
+ * Returns the key for a value from the context data.
+ * @param key the key to locate.
+ * @return the value or null if it is not found.
+ */
+ default String get(String key) {
+ return null;
+ }
+
+ /**
+ * Returns a Map containing context data to be injected into the event or null if no context data is to be added.
+ *
+ * Thread-safety note: The returned object can safely be passed off to another thread: future changes in the
+ * underlying context data will not be reflected in the returned object.
+ *
+ * @return A Map containing the context data or null.
+ */
Map supplyContextData();
/**
@@ -42,4 +57,35 @@ public interface ContextDataProvider extends org.apache.logging.log4j.spi.Contex
default StringMap supplyStringMap() {
return new JdkMapAdapterStringMap(supplyContextData(), true);
}
+
+ /**
+ * Returns the number of items in this context.
+ * @return the number of items in the context.
+ */
+ default int size() {
+ Map contextMap = supplyContextData();
+ return contextMap != null ? contextMap.size() : 0;
+ }
+
+ /**
+ * Add all the keys in the current context to the provided Map.
+ * @param map the StringMap to add the keys and values to.
+ */
+ default void addAll(Map map) {
+ Map contextMap = supplyContextData();
+ if (contextMap != null) {
+ map.putAll(contextMap);
+ }
+ }
+
+ /**
+ * Add all the keys in the current context to the provided StringMap.
+ * @param map the StringMap to add the keys and values to.
+ */
+ default void addAll(StringMap map) {
+ Map contextMap = supplyContextData();
+ if (contextMap != null) {
+ contextMap.forEach(map::putValue);
+ }
+ }
}
From 0291e46a912cbc2153c413a9284e2c41a7e478eb Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Thu, 4 Apr 2024 09:00:22 -0700
Subject: [PATCH 11/19] Revert unit test change. Correct site changes
---
.../message/ParameterizedMapMessageTest.java | 77 -----
.../message/ParameterizedMessageTest.java | 303 ++++++++++++++++++
.../modules/ROOT/pages/manual/extending.adoc | 2 +-
3 files changed, 304 insertions(+), 78 deletions(-)
delete mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMapMessageTest.java
create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageTest.java
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
deleted file mode 100644
index a560570846b..00000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMapMessageTest.java
+++ /dev/null
@@ -1,77 +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 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
new file mode 100644
index 00000000000..4bd5df91bef
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/message/ParameterizedMessageTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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 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:
+ *
+ * 1. The arguments contains an exception, and the count of placeholder is equal to arguments include exception.
+ * 2. The arguments contains an exception, and the count of placeholder is equal to arguments except exception.
+ * All of these should not logged in status logger.
+ *
+ *
+ * @return Streams
+ */
+ static Stream 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 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 statusDataList = statusListener.getStatusData().collect(Collectors.toList());
+ assertThat(statusDataList).hasSize(0);
+ }
+
+ /**
+ * In this test cases, constructed the following scenarios:
+ *
+ * 1. The placeholders are greater than the count of arguments.
+ * 2. The placeholders are less than the count of arguments.
+ * 3. The arguments contains an exception, and the placeholder is greater than normal arguments.
+ * 4. The arguments contains an exception, and the placeholder is less than the arguments.
+ * All of these should logged in status logger with WARN level.
+ *
+ *
+ * @return streams
+ */
+ static Stream 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 formattedMessageSupplier) {
+
+ // Verify the formatted message
+ final String formattedMessage = formattedMessageSupplier.get();
+ assertThat(formattedMessage).isEqualTo(expected);
+
+ // Verify the status logger warn
+ final List 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/src/site/antora/modules/ROOT/pages/manual/extending.adoc b/src/site/antora/modules/ROOT/pages/manual/extending.adoc
index f777ab2c19f..e9deb09dd70 100644
--- a/src/site/antora/modules/ROOT/pages/manual/extending.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/extending.adoc
@@ -590,7 +590,7 @@ additional key-value pairs into the LogEvent's context data. Log4j
uses `java.util.ServiceLoader` to locate and load `ContextDataProvider` instances.
Log4j itself adds the ThreadContext data to the LogEvent using
`org.apache.logging.log4j.core.impl.ThreadContextDataProvider`. Custom implementations
-should implement the `org.apache.logging.log4j.core.util.ContextDataProvider` interface and
+should implement the `org.apache.logging.log4j.core.util.ContextDataProvider` interface and
declare it as a service by defining the implmentation class in a file named
`META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider`.
From e7751cd966437e79de613d502ca9b1e2fe46d860 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Thu, 4 Apr 2024 09:06:15 -0700
Subject: [PATCH 12/19] Fix comment
---
.../java/org/apache/logging/log4j/message/package-info.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 393e7b517fc..e792d124a4c 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.24.0, to add ParameterizedMapMessage.
+ * Bumped to 2.24.0, since FormattedMessage behavior changede.
*/
@Version("2.24.0")
package org.apache.logging.log4j.message;
From c82a0830fc3a273331048cc8a6d0e86818b5e9d5 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Thu, 4 Apr 2024 10:06:09 -0700
Subject: [PATCH 13/19] Remove the Scopedcontext.Renderable interface
---
.../apache/logging/log4j/test/TestLogger.java | 4 +-
.../apache/logging/log4j/ScopedContext.java | 142 ++++++------------
.../logging/log4j/simple/SimpleLogger.java | 2 +-
.../core/impl/ScopedContextDataProvider.java | 4 +-
4 files changed, 48 insertions(+), 104 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 5f1fb02de7b..88e02cbbed4 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
@@ -81,10 +81,10 @@ protected void log(
sb.append(' ');
}
sb.append(message.getFormattedMessage());
- Map contextMap = ScopedContext.getContextMap();
+ Map contextMap = ScopedContext.getContextMap();
final Map mdc = new HashMap<>(ThreadContext.getImmutableContext());
if (contextMap != null && !contextMap.isEmpty()) {
- contextMap.forEach((key, value) -> mdc.put(key, value.render()));
+ contextMap.forEach((key, value) -> mdc.put(key, value.toString()));
}
if (!mdc.isEmpty()) {
sb.append(' ');
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
index 4081be5629b..8a57ac43619 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java
@@ -56,7 +56,7 @@ public class ScopedContext {
private static final ThreadLocal> scopedContext = new ThreadLocal<>();
/**
- * Returns an immutable Map containing all the key/value pairs as Renderable objects.
+ * Returns an immutable Map containing all the key/value pairs as Object objects.
* @return An immutable copy of the Map at the current scope.
*/
private static Optional getContext() {
@@ -99,9 +99,9 @@ private static void removeScopedContext() {
* @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.
+ * @return the Map of Object objects.
*/
- public static Map getContextMap() {
+ public static Map getContextMap() {
Optional context = getContext();
if (context.isPresent()
&& context.get().contextMap != null
@@ -129,13 +129,7 @@ public static int size() {
@SuppressWarnings("unchecked")
public static T get(String key) {
Optional context = getContext();
- if (context.isPresent()) {
- Renderable renderable = context.get().contextMap.get(key);
- if (renderable != null) {
- return (T) renderable.getObject();
- }
- }
- return null;
+ return context.map(instance -> (T) instance.contextMap.get(key)).orElse(null);
}
/**
@@ -146,9 +140,9 @@ public static T get(String key) {
public static String getString(String key) {
Optional context = getContext();
if (context.isPresent()) {
- Renderable renderable = context.get().contextMap.get(key);
- if (renderable != null) {
- return renderable.render();
+ Object obj = context.get().contextMap.get(key);
+ if (obj != null) {
+ return obj.toString();
}
}
return null;
@@ -161,9 +155,9 @@ public static String getString(String key) {
public static void addAll(Map map) {
Optional context = getContext();
if (context.isPresent()) {
- Map contextMap = context.get().contextMap;
+ Map contextMap = context.get().contextMap;
if (contextMap != null && !contextMap.isEmpty()) {
- contextMap.forEach((key, value) -> map.put(key, value.render()));
+ contextMap.forEach((key, value) -> map.put(key, value.toString()));
}
}
}
@@ -178,12 +172,11 @@ public static void addAll(Map map) {
*/
public static Instance where(String key, Object value) {
if (value != null) {
- Renderable renderable = value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value);
Instance parent = getContext().isPresent() ? getContext().get() : null;
- return new Instance(parent, key, renderable);
+ return new Instance(parent, key, value);
} else {
if (getContext().isPresent()) {
- Map map = getContextMap();
+ Map map = getContextMap();
map.remove(key);
return new Instance(map);
}
@@ -209,19 +202,18 @@ public static Instance where(String key, Supplier supplier) {
*/
public static Instance where(Map map) {
if (map != null && !map.isEmpty()) {
- Map renderableMap = new HashMap<>();
+ Map objectMap = new HashMap<>();
if (getContext().isPresent()) {
- renderableMap.putAll(getContext().get().contextMap);
+ objectMap.putAll(getContext().get().contextMap);
}
map.forEach((key, value) -> {
if (value == null || (value instanceof String && ((String) value).isEmpty())) {
- renderableMap.remove(key);
+ objectMap.remove(key);
} else {
- renderableMap.put(
- key, value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value));
+ objectMap.put(key, value);
}
});
- return new Instance(renderableMap);
+ return new Instance(objectMap);
} else {
return getContext().isPresent() ? getContext().get() : new Instance();
}
@@ -235,15 +227,14 @@ public static Instance where(Map map) {
*/
public static void runWhere(String key, Object obj, Runnable op) {
if (obj != null) {
- Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
- Map map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
- map.put(key, renderable);
+ map.put(key, obj);
new Instance(map).run(op);
} else {
- Map map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
@@ -261,12 +252,11 @@ public static void runWhere(String key, Object obj, Runnable op) {
*/
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 map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
- map.put(key, renderable);
+ map.put(key, obj);
if (executorService != null) {
return executorService.submit(new Runner(
new Instance(map), ThreadContext.getContext(), ThreadContext.getImmutableStack(), op));
@@ -275,7 +265,7 @@ public static Future> runWhere(String key, Object obj, ExecutorService executo
return CompletableFuture.completedFuture(0);
}
} else {
- Map map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
@@ -297,14 +287,12 @@ public static Future> runWhere(String key, Object obj, ExecutorService executo
*/
public static void runWhere(Map map, Runnable op) {
if (map != null && !map.isEmpty()) {
- Map renderableMap = new HashMap<>();
+ Map objectMap = new HashMap<>();
if (getContext().isPresent()) {
- renderableMap.putAll(getContext().get().contextMap);
+ objectMap.putAll(getContext().get().contextMap);
}
- map.forEach((key, value) -> {
- renderableMap.put(key, value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value));
- });
- new Instance(renderableMap).run(op);
+ objectMap.putAll(map);
+ new Instance(objectMap).run(op);
} else {
op.run();
}
@@ -318,15 +306,14 @@ public static void runWhere(Map map, Runnable op) {
*/
public static R callWhere(String key, Object obj, Callable op) throws Exception {
if (obj != null) {
- Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
- Map map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
- map.put(key, renderable);
+ map.put(key, obj);
return new Instance(map).call(op);
} else {
- Map map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
@@ -345,12 +332,11 @@ public static R callWhere(String key, Object obj, Callable op) throws Exc
public static Future callWhere(String key, Object obj, ExecutorService executorService, Callable op)
throws Exception {
if (obj != null) {
- Renderable renderable = obj instanceof Renderable ? (Renderable) obj : new ObjectRenderable(obj);
- Map map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
- map.put(key, renderable);
+ map.put(key, obj);
if (executorService != null) {
return executorService.submit(new Caller(
new Instance(map), ThreadContext.getContext(), ThreadContext.getImmutableStack(), op));
@@ -360,7 +346,7 @@ public static Future callWhere(String key, Object obj, ExecutorService ex
}
} else {
if (executorService != null) {
- Map map = new HashMap<>();
+ Map map = new HashMap<>();
if (getContext().isPresent()) {
map.putAll(getContext().get().contextMap);
}
@@ -381,14 +367,12 @@ public static Future callWhere(String key, Object obj, ExecutorService ex
*/
public static R callWhere(Map map, Callable op) throws Exception {
if (map != null && !map.isEmpty()) {
- Map renderableMap = new HashMap<>();
+ Map objectMap = new HashMap<>();
if (getContext().isPresent()) {
- renderableMap.putAll(getContext().get().contextMap);
+ objectMap.putAll(getContext().get().contextMap);
}
- map.forEach((key, value) -> {
- renderableMap.put(key, value instanceof Renderable ? (Renderable) value : new ObjectRenderable(value));
- });
- return new Instance(renderableMap).call(op);
+ objectMap.putAll(map);
+ return new Instance(objectMap).call(op);
} else {
return op.call();
}
@@ -398,8 +382,8 @@ public static class Instance {
private final Instance parent;
private final String key;
- private final Renderable value;
- private final Map contextMap;
+ private final Object value;
+ private final Map contextMap;
private Instance() {
this.parent = null;
@@ -408,14 +392,14 @@ private Instance() {
this.contextMap = null;
}
- private Instance(Map map) {
+ private Instance(Map map) {
this.parent = null;
this.key = null;
this.value = null;
this.contextMap = map;
}
- private Instance(Instance parent, String key, Renderable value) {
+ private Instance(Instance parent, String key, Object value) {
this.parent = parent;
this.key = key;
this.value = value;
@@ -446,8 +430,7 @@ public Instance where(String key, Supplier supplier) {
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 new Instance(this, key, obj);
}
return this;
}
@@ -495,7 +478,7 @@ public Future call(ExecutorService executorService, Callable op) {
}
private static class Runner implements Runnable {
- private final Map contextMap = new HashMap<>();
+ private final Map contextMap = new HashMap<>();
private final Map threadContextMap;
private final ThreadContext.ContextStack contextStack;
private final Instance context;
@@ -546,7 +529,7 @@ public void run() {
}
private static class Caller implements Callable {
- private final Map contextMap = new HashMap<>();
+ private final Map contextMap = new HashMap<>();
private final Instance context;
private final Map threadContextMap;
private final ThreadContext.ContextStack contextStack;
@@ -595,43 +578,4 @@ public R call() throws Exception {
}
}
}
-
- /**
- * Interface for converting Objects stored in the ContextScope to Strings for logging.
- *
- * Users implementing this interface are encouraged to make the render method as lightweight as possible,
- * Typically by creating the String representation of the object during its construction and just returning
- * the String.
- */
- 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/simple/SimpleLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/simple/SimpleLogger.java
index f5529f4258d..053ac45dcbb 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
@@ -297,7 +297,7 @@ public void logMessage(
sb.append(msg.getFormattedMessage());
if (showContextMap) {
final Map mdc = new HashMap<>(ThreadContext.getImmutableContext());
- ScopedContext.getContextMap().forEach((key, value) -> mdc.put(key, value.render()));
+ ScopedContext.getContextMap().forEach((key, value) -> mdc.put(key, value.toString()));
if (!mdc.isEmpty()) {
sb.append(SPACE);
sb.append(mdc.toString());
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
index 805c0979e7c..653e17b7cdf 100644
--- 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
@@ -38,10 +38,10 @@ public String get(String key) {
@Override
public Map supplyContextData() {
- Map contextMap = ScopedContext.getContextMap();
+ Map contextMap = ScopedContext.getContextMap();
if (!contextMap.isEmpty()) {
Map map = new HashMap<>();
- contextMap.forEach((key, value) -> map.put(key, value.render()));
+ contextMap.forEach((key, value) -> map.put(key, value.toString()));
return map;
} else {
return Collections.emptyMap();
From cf01425bf5319bda97d9dea8008609b81e675323 Mon Sep 17 00:00:00 2001
From: Ralph Goers
Date: Thu, 4 Apr 2024 13:48:27 -0700
Subject: [PATCH 14/19] Fix site conflicts
---
src/site/antora/modules/ROOT/nav.adoc | 2 ++
.../antora/modules/ROOT/pages/manual/resource-logger.adoc | 4 +---
.../antora/modules/ROOT/pages/manual/scoped-context.adoc | 8 +++-----
3 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/site/antora/modules/ROOT/nav.adoc b/src/site/antora/modules/ROOT/nav.adoc
index df86ec77465..92587f04c80 100644
--- a/src/site/antora/modules/ROOT/nav.adoc
+++ b/src/site/antora/modules/ROOT/nav.adoc
@@ -34,6 +34,8 @@
** xref:manual/eventlogging.adoc[]
** xref:manual/messages.adoc[]
** xref:manual/thread-context.adoc[]
+** xref:manual/scoped-context.adoc[]
+** xref:manual/resource-logger.adoc[]
** xref:manual/configuration.adoc[]
** xref:manual/usage.adoc[]
** xref:manual/cloud.adoc[]
diff --git a/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc b/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
index 289b69a3443..674de056aaf 100644
--- a/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/resource-logger.adoc
@@ -14,10 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
////
-= Log4j 2 API
-Ralph Goers ;
-== Resource Logging
+= 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.
diff --git a/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc b/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc
index 592945cf7e9..f80bc6f671f 100644
--- a/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/scoped-context.adoc
@@ -14,10 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
////
-= Log4j 2 API
-Ralph Goers ;
-== Scoped Context
+= 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.
@@ -60,7 +58,7 @@ 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 ===
+== 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
@@ -95,7 +93,7 @@ private class Worker implements Runnable {
ScopeContext also supports call methods in addition to run methods so the called functions can
directly return values.
-=== Nested ScopedContexts
+== 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
From 7f9c7e8ed70ef1f7fbb5789deb784bd0d2606b91 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Thu, 4 Apr 2024 23:46:52 +0200
Subject: [PATCH 15/19] Delegate `ScopedContext` functionality to interface
To provide more configurability for the `ScopedContext` service, this PR
moves its implementation details to `log4j-core` and replaces it with a
`ScopedContextProvider` interface.
In Log4j API only a NO-OP version of the provider is present, but each
implementation of the API can provide its own.
---
log4j-api-test/pom.xml | 11 +-
.../apache/logging/log4j/test/TestLogger.java | 5 +-
.../test/spi/ScopedContextProviderSuite.java | 178 +++++++
.../logging/log4j/ResourceLoggerTest.java | 2 +
.../logging/log4j/ScopedContextTest.java | 154 ------
.../apache/logging/log4j/ScopedContext.java | 487 ++----------------
.../logging/log4j/simple/SimpleLogger.java | 7 +-
.../apache/logging/log4j/spi/Provider.java | 7 +
.../log4j/spi/ScopedContextProvider.java | 81 +++
.../internal/NoopScopedContextProvider.java | 99 ++++
.../DefaultScopedContextProviderTest.java | 57 ++
.../log4j/core/impl/Log4jProvider.java | 17 +
.../core/impl/ScopedContextDataProvider.java | 20 +-
.../DefaultScopedContextProvider.java | 389 ++++++++++++++
14 files changed, 908 insertions(+), 606 deletions(-)
create mode 100644 log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ScopedContextProviderSuite.java
delete mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java
create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/NoopScopedContextProvider.java
create mode 100644 log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java
create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProvider.java
diff --git a/log4j-api-test/pom.xml b/log4j-api-test/pom.xml
index 0ef2578dd8f..40d4dc2525b 100644
--- a/log4j-api-test/pom.xml
+++ b/log4j-api-test/pom.xml
@@ -37,6 +37,7 @@
org.apache.logging.log4j.test
org.apache.commons.lang3.*;resolution:=optional,
+ org.assertj.*;resolution:=optional,
org.junit.*;resolution:=optional,
org.hamcrest.*;resolution:=optional,
@@ -48,6 +49,7 @@
junit;transitive=false,
+ org.assertj.core;transitive=false,
org.hamcrest;transitive=false,
org.junit.jupiter.api;transitive=false,
org.junitpioneer;transitive=false,
@@ -72,6 +74,10 @@
org.apache.logging.log4j
log4j-api
+
+ org.assertj
+ assertj-core
+
org.apache.commons
commons-lang3
@@ -108,11 +114,6 @@
org.codehaus.plexus
plexus-utils
-
- org.assertj
- assertj-core
- test
-
com.fasterxml.jackson.core
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 88e02cbbed4..d3f72171084 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
@@ -25,11 +25,11 @@
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;
import org.apache.logging.log4j.spi.AbstractLogger;
+import org.apache.logging.log4j.util.ProviderUtil;
/**
*
@@ -81,7 +81,8 @@ protected void log(
sb.append(' ');
}
sb.append(message.getFormattedMessage());
- Map contextMap = ScopedContext.getContextMap();
+ final Map contextMap =
+ ProviderUtil.getProvider().getScopedContextProvider().getContextMap();
final Map mdc = new HashMap<>(ThreadContext.getImmutableContext());
if (contextMap != null && !contextMap.isEmpty()) {
contextMap.forEach((key, value) -> mdc.put(key, value.toString()));
diff --git a/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ScopedContextProviderSuite.java b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ScopedContextProviderSuite.java
new file mode 100644
index 00000000000..dc0e7477763
--- /dev/null
+++ b/log4j-api-test/src/main/java/org/apache/logging/log4j/test/spi/ScopedContextProviderSuite.java
@@ -0,0 +1,178 @@
+/*
+ * 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.test.spi;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+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.apache.logging.log4j.ScopedContext;
+import org.apache.logging.log4j.spi.ScopedContextProvider;
+
+/**
+ * Provides test that should be passed by all implementations of {@link ScopedContextProviderSuite}.
+ * @since 2.24.0
+ */
+public abstract class ScopedContextProviderSuite {
+
+ private static ScopedContext.Instance where(
+ final ScopedContextProvider provider, final String key, final Object value) {
+ return provider.newScopedContext(key, value);
+ }
+
+ protected static void testScope(final ScopedContextProvider scopedContext) {
+ where(scopedContext, "key1", "Log4j2")
+ .run(() -> assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2"));
+ where(scopedContext, "key1", "value1").run(() -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
+ where(scopedContext, "key2", "value2").run(() -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
+ assertThat(scopedContext.getValue("key2")).isEqualTo("value2");
+ });
+ });
+ }
+
+ private static void runWhere(
+ final ScopedContextProvider provider, final String key, final Object value, final Runnable task) {
+ provider.newScopedContext(key, value).run(task);
+ }
+
+ private static Future runWhere(
+ final ScopedContextProvider provider,
+ final String key,
+ final Object value,
+ final ExecutorService executorService,
+ final Runnable task) {
+ return provider.newScopedContext(key, value).run(executorService, task);
+ }
+
+ protected static void testRunWhere(final ScopedContextProvider scopedContext) {
+ runWhere(scopedContext, "key1", "Log4j2", () -> assertThat(scopedContext.getValue("key1"))
+ .isEqualTo("Log4j2"));
+ runWhere(scopedContext, "key1", "value1", () -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
+ runWhere(scopedContext, "key2", "value2", () -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
+ assertThat(scopedContext.getValue("key2")).isEqualTo("value2");
+ });
+ });
+ }
+
+ protected static void testRunThreads(final ScopedContextProvider scopedContext) {
+ BlockingQueue 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);
+ runWhere(scopedContext, "key1", "Log4j2", () -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
+ Future> future = runWhere(scopedContext, "key2", "value2", executorService, () -> {
+ assertNotEquals(Thread.currentThread().getId(), id);
+ assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
+ counter.incrementAndGet();
+ });
+ assertDoesNotThrow(() -> {
+ future.get();
+ assertTrue(future.isDone());
+ assertThat(counter.get()).isEqualTo(1);
+ });
+ });
+ }
+
+ protected static void testThreads(final ScopedContextProvider scopedContext) throws Exception {
+ BlockingQueue 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);
+ where(scopedContext, "key1", "Log4j2").run(() -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
+ Future> future = where(scopedContext, "key2", "value2").run(executorService, () -> {
+ assertNotEquals(Thread.currentThread().getId(), id);
+ assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
+ counter.incrementAndGet();
+ });
+ assertDoesNotThrow(() -> {
+ future.get();
+ assertTrue(future.isDone());
+ assertThat(counter.get()).isEqualTo(1);
+ });
+ });
+ }
+
+ protected static void testThreadException(final ScopedContextProvider scopedContext) throws Exception {
+ BlockingQueue 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();
+ runWhere(scopedContext, "key1", "Log4j2", () -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
+ Future> future = where(scopedContext, "key2", "value2").run(executorService, () -> {
+ assertNotEquals(Thread.currentThread().getId(), id);
+ throw new NullPointerException("On purpose NPE");
+ });
+ assertThatThrownBy(future::get)
+ .hasRootCauseInstanceOf(NullPointerException.class)
+ .hasRootCauseMessage("On purpose NPE");
+ });
+ }
+
+ private static R callWhere(
+ final ScopedContextProvider provider, final String key, final Object value, final Callable task)
+ throws Exception {
+ return provider.newScopedContext(key, value).call(task);
+ }
+
+ private static Future callWhere(
+ final ScopedContextProvider provider,
+ final String key,
+ final Object value,
+ final ExecutorService executorService,
+ final Callable task) {
+ return provider.newScopedContext(key, value).call(executorService, task);
+ }
+
+ protected static void testThreadCall(final ScopedContextProvider scopedContext) throws Exception {
+ BlockingQueue 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 = callWhere(scopedContext, "key1", "Log4j2", () -> {
+ assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
+ Future