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 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 future = callWhere(scopedContext, "key2", "value2", executorService, () -> { + assertNotEquals(Thread.currentThread().getId(), id); + assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2"); + return counter.incrementAndGet(); + }); + Integer val = future.get(); + assertTrue(future.isDone()); + assertThat(counter.get()).isEqualTo(1); + return val; + }); + assertThat(returnVal).isEqualTo(1); + } +} 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 4822a52e123..c2273e27907 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 @@ -29,11 +29,13 @@ 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.Disabled; import org.junit.jupiter.api.Test; /** * Class Description goes here. */ +@Disabled("Does not work with the NO-OP implementation of ScopedContextProvider in the API.") public class ResourceLoggerTest { @BeforeAll public static void beforeAll() { 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 deleted file mode 100644 index d9ba5872e62..00000000000 --- a/log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java +++ /dev/null @@ -1,154 +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; - -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/src/main/java/org/apache/logging/log4j/ScopedContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java index 8a57ac43619..39621905722 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,151 +16,42 @@ */ 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; 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.status.StatusLogger; +import org.apache.logging.log4j.spi.ScopedContextProvider; +import org.apache.logging.log4j.util.ProviderUtil; /** * 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(); - - private static final ThreadLocal> scopedContext = new ThreadLocal<>(); - - /** - * Returns an immutable Map containing all the key/value pairs as Object objects. - * @return An immutable copy of the Map at the current scope. - */ - 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); - } +public final class ScopedContext { - /** - * 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 - * only be used by implementations of Log4j API. - * @return the Map of Object objects. - */ - public static Map getContextMap() { - Optional context = getContext(); - if (context.isPresent() - && context.get().contextMap != null - && !context.get().contextMap.isEmpty()) { - return Collections.unmodifiableMap(context.get().contextMap); - } - return Collections.emptyMap(); - } + private static final ScopedContextProvider provider = + ProviderUtil.getProvider().getScopedContextProvider(); - /** - * @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 = 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 = getContext(); - return context.map(instance -> (T) instance.contextMap.get(key)).orElse(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 = getContext(); - if (context.isPresent()) { - Object obj = context.get().contextMap.get(key); - if (obj != null) { - return obj.toString(); - } - } - 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 = getContext(); - if (context.isPresent()) { - Map contextMap = context.get().contextMap; - if (contextMap != null && !contextMap.isEmpty()) { - contextMap.forEach((key, value) -> map.put(key, value.toString())); - } - } - } + private ScopedContext() {} /** * Creates a ScopedContext Instance with a key/value pair. @@ -170,18 +61,8 @@ public static void addAll(Map map) { * @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) { - Instance parent = getContext().isPresent() ? getContext().get() : null; - return new Instance(parent, key, value); - } else { - if (getContext().isPresent()) { - Map map = getContextMap(); - map.remove(key); - return new Instance(map); - } - } - return getContext().isPresent() ? getContext().get() : new Instance(); + public static Instance where(final String key, final Object value) { + return provider.newScopedContext(key, value); } /** @@ -191,7 +72,7 @@ public static Instance where(String key, Object value) { * @param supplier the function to generate the value. * @return the ScopedContext being constructed. */ - public static Instance where(String key, Supplier supplier) { + public static Instance where(final String key, final Supplier supplier) { return where(key, supplier.get()); } @@ -200,211 +81,76 @@ public static Instance where(String key, Supplier supplier) { * @param map the Map. * @return the ScopedContext Instance constructed. */ - public static Instance where(Map map) { - if (map != null && !map.isEmpty()) { - Map objectMap = new HashMap<>(); - if (getContext().isPresent()) { - objectMap.putAll(getContext().get().contextMap); - } - map.forEach((key, value) -> { - if (value == null || (value instanceof String && ((String) value).isEmpty())) { - objectMap.remove(key); - } else { - objectMap.put(key, value); - } - }); - return new Instance(objectMap); - } else { - return getContext().isPresent() ? getContext().get() : new Instance(); - } + public static Instance where(final Map map) { + return provider.newScopedContext(map); } /** * 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. + * @param value the value associated with the key. + * @param task the Runnable to call. */ - public static void runWhere(String key, Object obj, Runnable op) { - if (obj != null) { - Map map = new HashMap<>(); - if (getContext().isPresent()) { - map.putAll(getContext().get().contextMap); - } - map.put(key, obj); - new Instance(map).run(op); - } else { - Map map = new HashMap<>(); - if (getContext().isPresent()) { - map.putAll(getContext().get().contextMap); - } - map.remove(key); - new Instance(map).run(op); - } + public static void runWhere(final String key, final Object value, final Runnable task) { + provider.newScopedContext(key, value).run(task); } /** * 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 value the value associated with the key. * @param executorService the ExecutorService to dispatch the work. - * @param op the Runnable to call. + * @param task the Runnable to call. */ - public static Future runWhere(String key, Object obj, ExecutorService executorService, Runnable op) { - if (obj != null) { - Map map = new HashMap<>(); - if (getContext().isPresent()) { - map.putAll(getContext().get().contextMap); - } - map.put(key, obj); - 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 (getContext().isPresent()) { - map.putAll(getContext().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); - } - } + public static Future runWhere( + final String key, final Object value, final ExecutorService executorService, final Runnable task) { + return provider.newScopedContext(key, value).run(executorService, task); } /** * Creates a ScopedContext with a Map of keys and values and calls a method. * @param map the Map. - * @param op the Runnable to call. + * @param task the Runnable to call. */ - public static void runWhere(Map map, Runnable op) { - if (map != null && !map.isEmpty()) { - Map objectMap = new HashMap<>(); - if (getContext().isPresent()) { - objectMap.putAll(getContext().get().contextMap); - } - objectMap.putAll(map); - new Instance(objectMap).run(op); - } else { - op.run(); - } + public static void runWhere(final Map map, final Runnable task) { + provider.newScopedContext(map).run(task); } /** * 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. + * @param value the value associated with the key. + * @param task the Runnable to call. */ - public static R callWhere(String key, Object obj, Callable op) throws Exception { - if (obj != null) { - Map map = new HashMap<>(); - if (getContext().isPresent()) { - map.putAll(getContext().get().contextMap); - } - map.put(key, obj); - return new Instance(map).call(op); - } else { - Map map = new HashMap<>(); - if (getContext().isPresent()) { - map.putAll(getContext().get().contextMap); - } - map.remove(key); - return new Instance(map).call(op); - } + public static R callWhere(final String key, final Object value, final Callable task) throws Exception { + return provider.newScopedContext(key, value).call(task); } /** * 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 value the value associated with the key. * @param executorService the ExecutorService to dispatch the work. - * @param op the Callable to call. + * @param task the Callable to call. */ - public static Future callWhere(String key, Object obj, ExecutorService executorService, Callable op) - throws Exception { - if (obj != null) { - Map map = new HashMap<>(); - if (getContext().isPresent()) { - map.putAll(getContext().get().contextMap); - } - map.put(key, obj); - 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 (getContext().isPresent()) { - map.putAll(getContext().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); - } - } + public static Future callWhere( + final String key, final Object value, final ExecutorService executorService, final Callable task) { + return provider.newScopedContext(key, value).call(executorService, task); } /** * Creates a ScopedContext with a Map of keys and values and calls a method. * @param map the Map. - * @param op the Runnable to call. + * @param task the Runnable to call. */ - public static R callWhere(Map map, Callable op) throws Exception { - if (map != null && !map.isEmpty()) { - Map objectMap = new HashMap<>(); - if (getContext().isPresent()) { - objectMap.putAll(getContext().get().contextMap); - } - objectMap.putAll(map); - return new Instance(objectMap).call(op); - } else { - return op.call(); - } + public static R callWhere(final Map map, final Callable task) throws Exception { + return provider.newScopedContext(map).call(task); } - public static class Instance { - - private final Instance parent; - private final String key; - private final Object 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, Object value) { - this.parent = parent; - this.key = key; - this.value = value; - this.contextMap = null; - } + /** + * A holder of scoped context data. + */ + public interface Instance { /** * Adds a key/value pair to the ScopedContext being constructed. @@ -413,9 +159,7 @@ private Instance(Instance parent, String key, Object value) { * @param value the value associated with the key. * @return the ScopedContext being constructed. */ - public Instance where(String key, Object value) { - return addObject(key, value); - } + Instance where(String key, Object value); /** * Adds a key/value pair to the ScopedContext being constructed. @@ -424,158 +168,37 @@ public Instance where(String key, Object value) { * @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) { - return new Instance(this, key, obj); - } - return this; - } + Instance where(String key, Supplier supplier); /** * Executes a code block that includes all the key/value pairs added to the ScopedContext. * - * @param op the code block to execute. + * @param task the code block to execute. */ - public void run(Runnable op) { - new Runner(this, null, null, op).run(); - } + void run(Runnable task); /** * 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. + * @param task 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)); - } + Future run(ExecutorService executorService, Runnable task); /** * Executes a code block that includes all the key/value pairs added to the ScopedContext. * - * @param op the code block to execute. + * @param task 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(); - } + R call(Callable task) throws Exception; /** * 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. + * @param task 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); - } - addScopedContext(scopedContext); - try { - op.run(); - } finally { - 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); - } - addScopedContext(scopedContext); - try { - return op.call(); - } finally { - removeScopedContext(); - ThreadContext.clearAll(); - } - } + Future call(ExecutorService executorService, Callable task); } } 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 053ac45dcbb..1690893187f 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,11 +21,9 @@ 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; @@ -296,9 +294,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.toString())); - if (!mdc.isEmpty()) { + final Map mdc = ThreadContext.getImmutableContext(); + if (mdc.size() > 0) { sb.append(SPACE); sb.append(mdc.toString()); sb.append(SPACE); diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java index bca1a9ecae3..3eff6e8b3dc 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java @@ -434,6 +434,13 @@ public ThreadContextMap getThreadContextMapInstance() { return threadContextMapLazy.get(); } + /** + * @return An implementation of the {@link ScopedContextProvider} service to use. + */ + public ScopedContextProvider getScopedContextProvider() { + return ScopedContextProvider.noop(); + } + /** * Gets the URL containing this Provider's Log4j details. * diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java new file mode 100644 index 00000000000..e2281e3c344 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java @@ -0,0 +1,81 @@ +/* + * 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.ScopedContext; +import org.apache.logging.log4j.spi.internal.NoopScopedContextProvider; +import org.apache.logging.log4j.util.StringMap; + +/** + * The service underlying {@link ScopedContext}. + * @since 2.24.0 + */ +public interface ScopedContextProvider { + + static ScopedContextProvider noop() { + return NoopScopedContextProvider.SCOPED_CONTEXT_PROVIDER_INSTANCE; + } + + /** + * @return An immutable map with the current context data. + */ + Map getContextMap(); + + /** + * Adds the current data to the provided {@link StringMap}. + * @param map The {@link StringMap} to add data to. + */ + default void addContextMapTo(final StringMap map) { + getContextMap().forEach(map::putValue); + } + + /** + * 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. + */ + Object getValue(String key); + + /** + * 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 converted to {@link String}. + */ + String getString(String key); + + /** + * Creates a new context containing the current context data from {@link org.apache.logging.log4j.ThreadContext}. + * @return A new instance of a scoped context. + */ + ScopedContext.Instance newScopedContext(); + + /** + * Creates a new context containing the current context data from {@link org.apache.logging.log4j.ThreadContext}. + * @param key An additional key for the context. + * @param value An additional value for the context. + * @return A new instance of a scoped context. + */ + ScopedContext.Instance newScopedContext(String key, Object value); + + /** + * Creates a new context containing the current context data from {@link org.apache.logging.log4j.ThreadContext}. + * @param map Additional data to include in the context. + * @return A new instance of a scoped context. + */ + ScopedContext.Instance newScopedContext(Map map); +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/NoopScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/NoopScopedContextProvider.java new file mode 100644 index 00000000000..65c5376102a --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/NoopScopedContextProvider.java @@ -0,0 +1,99 @@ +/* + * 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.internal; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import org.apache.logging.log4j.ScopedContext; +import org.apache.logging.log4j.spi.ScopedContextProvider; + +/** + * An implementation of {@link ScopedContextProvider} that does not propagate any data. + * @since 2.24.0 + */ +public class NoopScopedContextProvider implements ScopedContextProvider { + + private static final ScopedContext.Instance SCOPED_CONTEXT_INSTANCE = new NoopInstance(); + public static final ScopedContextProvider SCOPED_CONTEXT_PROVIDER_INSTANCE = new NoopScopedContextProvider(); + + @Override + public Map getContextMap() { + return Collections.emptyMap(); + } + + @Override + public Object getValue(final String key) { + return null; + } + + @Override + public String getString(final String key) { + return null; + } + + @Override + public ScopedContext.Instance newScopedContext() { + return SCOPED_CONTEXT_INSTANCE; + } + + @Override + public ScopedContext.Instance newScopedContext(final String key, final Object value) { + return SCOPED_CONTEXT_INSTANCE; + } + + @Override + public ScopedContext.Instance newScopedContext(final Map map) { + return SCOPED_CONTEXT_INSTANCE; + } + + private static class NoopInstance implements ScopedContext.Instance { + + @Override + public ScopedContext.Instance where(final String key, final Object value) { + return this; + } + + @Override + public ScopedContext.Instance where(final String key, final Supplier supplier) { + return this; + } + + @Override + public void run(final Runnable task) { + task.run(); + } + + @Override + public Future run(final ExecutorService executorService, final Runnable task) { + return executorService.submit(task, null); + } + + @Override + public R call(final Callable task) throws Exception { + return task.call(); + } + + @Override + public Future call(final ExecutorService executorService, final Callable task) { + return executorService.submit(task); + } + } +} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java new file mode 100644 index 00000000000..2ca67c1e239 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.impl.internal; + +import org.apache.logging.log4j.test.spi.ScopedContextProviderSuite; +import org.junit.jupiter.api.Test; + +class DefaultScopedContextProviderTest extends ScopedContextProviderSuite { + + private static DefaultScopedContextProvider createProvider() { + return new DefaultScopedContextProvider(); + } + + @Test + void testScope() { + testScope(createProvider()); + } + + @Test + void testRunWhere() { + testRunWhere(createProvider()); + } + + @Test + void testRunThreads() { + testRunThreads(createProvider()); + } + + @Test + void testThreads() throws Exception { + testThreads(createProvider()); + } + + @Test + void testThreadException() throws Exception { + testThreadException(createProvider()); + } + + @Test + void testThreadCall() throws Exception { + testThreadCall(createProvider()); + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java index bd0b62337cb..cd4ddfbe32b 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java @@ -17,15 +17,32 @@ package org.apache.logging.log4j.core.impl; import aQute.bnd.annotation.Resolution; +import aQute.bnd.annotation.spi.ServiceConsumer; import aQute.bnd.annotation.spi.ServiceProvider; +import java.util.ServiceLoader; +import org.apache.logging.log4j.core.impl.internal.DefaultScopedContextProvider; import org.apache.logging.log4j.spi.Provider; +import org.apache.logging.log4j.spi.ScopedContextProvider; +import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.ServiceLoaderUtil; /** * Binding for the Log4j API. */ @ServiceProvider(value = Provider.class, resolution = Resolution.OPTIONAL) +@ServiceConsumer(value = ScopedContextProvider.class, resolution = Resolution.OPTIONAL) public class Log4jProvider extends Provider { public Log4jProvider() { super(10, CURRENT_VERSION, Log4jContextFactory.class); } + + @Override + public ScopedContextProvider getScopedContextProvider() { + return ServiceLoaderUtil.safeStream( + ScopedContextProvider.class, + ServiceLoader.load(ScopedContextProvider.class), + StatusLogger.getLogger()) + .findFirst() + .orElse(DefaultScopedContextProvider.INSTANCE); + } } 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 653e17b7cdf..948d9c04b66 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 @@ -21,8 +21,9 @@ 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.spi.ScopedContextProvider; +import org.apache.logging.log4j.util.ProviderUtil; /** * ContextDataProvider for {@code Map} data. @@ -31,16 +32,19 @@ @ServiceProvider(value = ContextDataProvider.class, resolution = Resolution.OPTIONAL) public class ScopedContextDataProvider implements ContextDataProvider { + private final ScopedContextProvider scopedContext = + ProviderUtil.getProvider().getScopedContextProvider(); + @Override - public String get(String key) { - return ScopedContext.getString(key); + public String get(final String key) { + return scopedContext.getString(key); } @Override public Map supplyContextData() { - Map contextMap = ScopedContext.getContextMap(); + final Map contextMap = scopedContext.getContextMap(); if (!contextMap.isEmpty()) { - Map map = new HashMap<>(); + final Map map = new HashMap<>(); contextMap.forEach((key, value) -> map.put(key, value.toString())); return map; } else { @@ -50,11 +54,11 @@ public Map supplyContextData() { @Override public int size() { - return ScopedContext.size(); + return scopedContext.getContextMap().size(); } @Override - public void addAll(Map map) { - ScopedContext.addAll(map); + public void addAll(final Map map) { + scopedContext.getContextMap().forEach((key, value) -> map.put(key, String.valueOf(value))); } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProvider.java new file mode 100644 index 00000000000..ea9465337d6 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProvider.java @@ -0,0 +1,389 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.impl.internal; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ScopedContext; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.spi.ScopedContextProvider; +import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.StringMap; + +public class DefaultScopedContextProvider implements ScopedContextProvider { + + public static final Logger LOGGER = StatusLogger.getLogger(); + public static final ScopedContextProvider INSTANCE = new DefaultScopedContextProvider(); + + private final ThreadLocal> scopedContext = new ThreadLocal<>(); + + private final Instance EMPTY_INSTANCE = new Instance(this); + + /** + * 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 Optional getContext() { + final Deque stack = scopedContext.get(); + return stack != null ? Optional.of(stack.getFirst()) : Optional.empty(); + } + + /** + * Add the ScopeContext. + * @param context The ScopeContext. + */ + private void addScopedContext(final Instance context) { + Deque stack = scopedContext.get(); + if (stack == null) { + stack = new ArrayDeque<>(); + scopedContext.set(stack); + } + stack.addFirst(context); + } + + /** + * Remove the top ScopeContext. + */ + private void removeScopedContext() { + final Deque stack = scopedContext.get(); + if (stack != null) { + if (!stack.isEmpty()) { + stack.removeFirst(); + } + if (stack.isEmpty()) { + scopedContext.remove(); + } + } + } + + @Override + public Map getContextMap() { + final Optional context = getContext(); + return context.isPresent() + && context.get().contextMap != null + && !context.get().contextMap.isEmpty() + ? Collections.unmodifiableMap(context.get().contextMap) + : Collections.emptyMap(); + } + + /** + * 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. + */ + @Override + public Object getValue(final String key) { + final Optional context = getContext(); + return context.map(instance -> instance.contextMap) + .map(map -> map.get(key)) + .orElse(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. + */ + @Override + public String getString(final String key) { + final Optional context = getContext(); + if (context.isPresent()) { + final Object obj = context.get().contextMap.get(key); + if (obj != null) { + return obj.toString(); + } + } + return null; + } + + /** + * Adds all the String rendered objects in the context map to the provided Map. + * @param map The Map to add entries to. + */ + @Override + public void addContextMapTo(final StringMap map) { + final Optional context = getContext(); + if (context.isPresent()) { + final Map contextMap = context.get().contextMap; + if (contextMap != null && !contextMap.isEmpty()) { + contextMap.forEach((key, value) -> map.putValue(key, value.toString())); + } + } + } + + @Override + public ScopedContext.Instance newScopedContext() { + return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + } + + /** + * 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. + */ + @Override + public ScopedContext.Instance newScopedContext(final String key, final Object value) { + if (value != null) { + final Instance parent = getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + return new Instance(parent, key, value); + } else { + if (getContext().isPresent()) { + final Map map = getContextMap(); + map.remove(key); + return new Instance(this, map); + } + } + return newScopedContext(); + } + + /** + * Creates a ScopedContext Instance with a Map of keys and values. + * @param map the Map. + * @return the ScopedContext Instance constructed. + */ + @Override + public ScopedContext.Instance newScopedContext(final Map map) { + if (map != null && !map.isEmpty()) { + final Map objectMap = new HashMap<>(); + if (getContext().isPresent()) { + objectMap.putAll(getContext().get().contextMap); + } + map.forEach((key, value) -> { + if (value == null || (value instanceof String && ((String) value).isEmpty())) { + objectMap.remove(key); + } else { + objectMap.put(key, value); + } + }); + return new Instance(this, objectMap); + } else { + return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + } + } + + private static void setupContext( + final Map contextMap, + final Map threadContextMap, + final Collection contextStack, + final Instance context) { + 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(context.getProvider(), contextMap); + } + if (threadContextMap != null && !threadContextMap.isEmpty()) { + ThreadContext.putAll(threadContextMap); + } + if (contextStack != null) { + ThreadContext.setStack(contextStack); + } + context.getProvider().addScopedContext(scopedContext); + } + + private static final class Instance implements ScopedContext.Instance { + + private final DefaultScopedContextProvider provider; + private final Instance parent; + private final String key; + private final Object value; + private final Map contextMap; + + private Instance(final DefaultScopedContextProvider provider) { + this.provider = provider; + parent = null; + key = null; + value = null; + contextMap = null; + } + + private Instance(final DefaultScopedContextProvider provider, final Map map) { + this.provider = provider; + parent = null; + key = null; + value = null; + contextMap = map; + } + + private Instance(final Instance parent, final String key, final Object value) { + provider = parent.getProvider(); + this.parent = parent; + this.key = key; + this.value = value; + 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. + */ + @Override + public Instance where(final String key, final 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. + */ + @Override + public Instance where(final String key, final Supplier supplier) { + return addObject(key, supplier.get()); + } + + private Instance addObject(final String key, final Object obj) { + return obj != null ? new Instance(this, key, obj) : this; + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * + * @param task the code block to execute. + */ + @Override + public void run(final Runnable task) { + new Runner(this, null, null, task).run(); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + @Override + public Future run(final ExecutorService executorService, final Runnable task) { + return executorService.submit( + new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task), null); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * + * @param task the code block to execute. + * @return the return value from the code block. + */ + @Override + public R call(final Callable task) throws Exception { + return new Caller<>(this, null, null, task).call(); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + @Override + public Future call(final ExecutorService executorService, final Callable task) { + return executorService.submit( + new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task)); + } + + private DefaultScopedContextProvider getProvider() { + return provider; + } + } + + 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( + final Instance context, + final Map threadContextMap, + final ThreadContext.ContextStack contextStack, + final Runnable op) { + this.context = context; + this.threadContextMap = threadContextMap; + this.contextStack = contextStack; + this.op = op; + } + + @Override + public void run() { + setupContext(contextMap, threadContextMap, contextStack, context); + try { + op.run(); + } finally { + context.getProvider().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( + final Instance context, + final Map threadContextMap, + final ThreadContext.ContextStack contextStack, + final Callable op) { + this.context = context; + this.threadContextMap = threadContextMap; + this.contextStack = contextStack; + this.op = op; + } + + @Override + public R call() throws Exception { + setupContext(contextMap, threadContextMap, contextStack, context); + try { + return op.call(); + } finally { + context.getProvider().removeScopedContext(); + ThreadContext.clearAll(); + } + } + } +} From e28affa709abeade0a449a3a00aedbf2851790cb Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Thu, 4 Apr 2024 23:21:20 -0700 Subject: [PATCH 16/19] Incorporate Piotr's changes - mostly --- .../logging/log4j/ScopedContextTest.java | 154 +++++++ .../apache/logging/log4j/ScopedContext.java | 21 + .../logging/log4j/simple/SimpleLogger.java | 5 + .../apache/logging/log4j/spi/Provider.java | 2 +- .../log4j/spi/ScopedContextProvider.java | 6 +- .../DefaultScopedContextProvider.java | 60 +-- .../internal/NoopScopedContextProvider.java | 99 ----- .../DefaultScopedContextProviderTest.java | 4 +- .../log4j/core/impl/Log4jProvider.java | 4 +- .../internal/QueuedScopedContextProvider.java | 389 ++++++++++++++++++ 10 files changed, 612 insertions(+), 132 deletions(-) create mode 100644 log4j-api-test/src/test/java/org/apache/logging/log4j/ScopedContextTest.java rename {log4j-core/src/main/java/org/apache/logging/log4j/core/impl => log4j-api/src/main/java/org/apache/logging/log4j/spi}/internal/DefaultScopedContextProvider.java (91%) delete mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/NoopScopedContextProvider.java create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java 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/src/main/java/org/apache/logging/log4j/ScopedContext.java b/log4j-api/src/main/java/org/apache/logging/log4j/ScopedContext.java index 39621905722..37cb26e0940 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 @@ -147,6 +147,27 @@ public static R callWhere(final Map map, final Callable task) return provider.newScopedContext(map).call(task); } + /** + * Return the object with the specified key from the current context. + * @param key the key. + * @return the value of the key or null. + * @param The type of object expected. + * @throws ClassCastException if the specified type does not match the object stored. + */ + @SuppressWarnings("unchecked") + public static T get(String key) { + return (T) provider.getValue(key); + } + + /** + * 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) { + return provider.getString(key); + } + /** * A holder of scoped context data. */ 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..20471b82d07 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 @@ -28,7 +28,9 @@ 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.ScopedContextProvider; import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.ProviderUtil; import org.apache.logging.log4j.util.Strings; /** @@ -57,6 +59,8 @@ public class SimpleLogger extends AbstractLogger { private PrintStream stream; private final String logName; + private final ScopedContextProvider scopedContextProvider = + ProviderUtil.getProvider().getScopedContextProvider(); public SimpleLogger( final String name, @@ -295,6 +299,7 @@ public void logMessage( sb.append(msg.getFormattedMessage()); if (showContextMap) { final Map mdc = ThreadContext.getImmutableContext(); + scopedContextProvider.getContextMap().forEach((key, value) -> mdc.put(key, value.toString())); if (mdc.size() > 0) { sb.append(SPACE); sb.append(mdc.toString()); diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java index 3eff6e8b3dc..cb189942593 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/Provider.java @@ -438,7 +438,7 @@ public ThreadContextMap getThreadContextMapInstance() { * @return An implementation of the {@link ScopedContextProvider} service to use. */ public ScopedContextProvider getScopedContextProvider() { - return ScopedContextProvider.noop(); + return ScopedContextProvider.simple(); } /** diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java index e2281e3c344..5c14f036d95 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java @@ -18,7 +18,7 @@ import java.util.Map; import org.apache.logging.log4j.ScopedContext; -import org.apache.logging.log4j.spi.internal.NoopScopedContextProvider; +import org.apache.logging.log4j.spi.internal.DefaultScopedContextProvider; import org.apache.logging.log4j.util.StringMap; /** @@ -27,8 +27,8 @@ */ public interface ScopedContextProvider { - static ScopedContextProvider noop() { - return NoopScopedContextProvider.SCOPED_CONTEXT_PROVIDER_INSTANCE; + static ScopedContextProvider simple() { + return DefaultScopedContextProvider.INSTANCE; } /** diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java similarity index 91% rename from log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProvider.java rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java index ea9465337d6..fd5417f1673 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProvider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java @@ -14,12 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.logging.log4j.core.impl.internal; +package org.apache.logging.log4j.spi.internal; -import java.util.ArrayDeque; import java.util.Collection; import java.util.Collections; -import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -34,22 +32,23 @@ import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.StringMap; +/** + * An implementation of {@link ScopedContextProvider} that uses the simplest implementation. + * @since 2.24.0 + */ public class DefaultScopedContextProvider implements ScopedContextProvider { public static final Logger LOGGER = StatusLogger.getLogger(); public static final ScopedContextProvider INSTANCE = new DefaultScopedContextProvider(); - private final ThreadLocal> scopedContext = new ThreadLocal<>(); - - private final Instance EMPTY_INSTANCE = new Instance(this); + private final ThreadLocal scopedContext = new ThreadLocal<>(); /** * Returns an immutable Map containing all the key/value pairs as Object objects. - * @return An immutable copy of the Map at the current scope. + * @return The current context Instance. */ private Optional getContext() { - final Deque stack = scopedContext.get(); - return stack != null ? Optional.of(stack.getFirst()) : Optional.empty(); + return Optional.ofNullable(scopedContext.get()); } /** @@ -57,24 +56,19 @@ private Optional getContext() { * @param context The ScopeContext. */ private void addScopedContext(final Instance context) { - Deque stack = scopedContext.get(); - if (stack == null) { - stack = new ArrayDeque<>(); - scopedContext.set(stack); - } - stack.addFirst(context); + Instance current = scopedContext.get(); + scopedContext.set(context); } /** * Remove the top ScopeContext. */ private void removeScopedContext() { - final Deque stack = scopedContext.get(); - if (stack != null) { - if (!stack.isEmpty()) { - stack.removeFirst(); - } - if (stack.isEmpty()) { + final Instance current = scopedContext.get(); + if (current != null) { + if (current.parent != null) { + scopedContext.set(current.parent); + } else { scopedContext.remove(); } } @@ -137,7 +131,7 @@ public void addContextMapTo(final StringMap map) { @Override public ScopedContext.Instance newScopedContext() { - return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + return getContext().isPresent() ? getContext().get() : null; } /** @@ -151,8 +145,12 @@ public ScopedContext.Instance newScopedContext() { @Override public ScopedContext.Instance newScopedContext(final String key, final Object value) { if (value != null) { - final Instance parent = getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; - return new Instance(parent, key, value); + final Instance parent = getContext().isPresent() ? getContext().get() : null; + if (parent != null) { + return new Instance(parent, key, value); + } else { + return new Instance(this, key, value); + } } else { if (getContext().isPresent()) { final Map map = getContextMap(); @@ -184,7 +182,7 @@ public ScopedContext.Instance newScopedContext(final Map map) { }); return new Instance(this, objectMap); } else { - return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + return getContext().isPresent() ? getContext().get() : null; } } @@ -241,6 +239,14 @@ private Instance(final DefaultScopedContextProvider provider, final Map getContextMap() { - return Collections.emptyMap(); - } - - @Override - public Object getValue(final String key) { - return null; - } - - @Override - public String getString(final String key) { - return null; - } - - @Override - public ScopedContext.Instance newScopedContext() { - return SCOPED_CONTEXT_INSTANCE; - } - - @Override - public ScopedContext.Instance newScopedContext(final String key, final Object value) { - return SCOPED_CONTEXT_INSTANCE; - } - - @Override - public ScopedContext.Instance newScopedContext(final Map map) { - return SCOPED_CONTEXT_INSTANCE; - } - - private static class NoopInstance implements ScopedContext.Instance { - - @Override - public ScopedContext.Instance where(final String key, final Object value) { - return this; - } - - @Override - public ScopedContext.Instance where(final String key, final Supplier supplier) { - return this; - } - - @Override - public void run(final Runnable task) { - task.run(); - } - - @Override - public Future run(final ExecutorService executorService, final Runnable task) { - return executorService.submit(task, null); - } - - @Override - public R call(final Callable task) throws Exception { - return task.call(); - } - - @Override - public Future call(final ExecutorService executorService, final Callable task) { - return executorService.submit(task); - } - } -} diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java index 2ca67c1e239..a5957fc0840 100644 --- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/internal/DefaultScopedContextProviderTest.java @@ -21,8 +21,8 @@ class DefaultScopedContextProviderTest extends ScopedContextProviderSuite { - private static DefaultScopedContextProvider createProvider() { - return new DefaultScopedContextProvider(); + private static QueuedScopedContextProvider createProvider() { + return new QueuedScopedContextProvider(); } @Test diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java index cd4ddfbe32b..2142cf96dbe 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jProvider.java @@ -20,7 +20,7 @@ import aQute.bnd.annotation.spi.ServiceConsumer; import aQute.bnd.annotation.spi.ServiceProvider; import java.util.ServiceLoader; -import org.apache.logging.log4j.core.impl.internal.DefaultScopedContextProvider; +import org.apache.logging.log4j.core.impl.internal.QueuedScopedContextProvider; import org.apache.logging.log4j.spi.Provider; import org.apache.logging.log4j.spi.ScopedContextProvider; import org.apache.logging.log4j.status.StatusLogger; @@ -43,6 +43,6 @@ public ScopedContextProvider getScopedContextProvider() { ServiceLoader.load(ScopedContextProvider.class), StatusLogger.getLogger()) .findFirst() - .orElse(DefaultScopedContextProvider.INSTANCE); + .orElse(QueuedScopedContextProvider.INSTANCE); } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java new file mode 100644 index 00000000000..d5452135b56 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java @@ -0,0 +1,389 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.logging.log4j.core.impl.internal; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ScopedContext; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.spi.ScopedContextProvider; +import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.StringMap; + +public class QueuedScopedContextProvider implements ScopedContextProvider { + + public static final Logger LOGGER = StatusLogger.getLogger(); + public static final ScopedContextProvider INSTANCE = new QueuedScopedContextProvider(); + + private final ThreadLocal> scopedContext = new ThreadLocal<>(); + + private final Instance EMPTY_INSTANCE = new Instance(this); + + /** + * 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 Optional getContext() { + final Deque stack = scopedContext.get(); + return stack != null ? Optional.of(stack.getFirst()) : Optional.empty(); + } + + /** + * Add the ScopeContext. + * @param context The ScopeContext. + */ + private void addScopedContext(final Instance context) { + Deque stack = scopedContext.get(); + if (stack == null) { + stack = new ArrayDeque<>(); + scopedContext.set(stack); + } + stack.addFirst(context); + } + + /** + * Remove the top ScopeContext. + */ + private void removeScopedContext() { + final Deque stack = scopedContext.get(); + if (stack != null) { + if (!stack.isEmpty()) { + stack.removeFirst(); + } + if (stack.isEmpty()) { + scopedContext.remove(); + } + } + } + + @Override + public Map getContextMap() { + final Optional context = getContext(); + return context.isPresent() + && context.get().contextMap != null + && !context.get().contextMap.isEmpty() + ? Collections.unmodifiableMap(context.get().contextMap) + : Collections.emptyMap(); + } + + /** + * 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. + */ + @Override + public Object getValue(final String key) { + final Optional context = getContext(); + return context.map(instance -> instance.contextMap) + .map(map -> map.get(key)) + .orElse(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. + */ + @Override + public String getString(final String key) { + final Optional context = getContext(); + if (context.isPresent()) { + final Object obj = context.get().contextMap.get(key); + if (obj != null) { + return obj.toString(); + } + } + return null; + } + + /** + * Adds all the String rendered objects in the context map to the provided Map. + * @param map The Map to add entries to. + */ + @Override + public void addContextMapTo(final StringMap map) { + final Optional context = getContext(); + if (context.isPresent()) { + final Map contextMap = context.get().contextMap; + if (contextMap != null && !contextMap.isEmpty()) { + contextMap.forEach((key, value) -> map.putValue(key, value.toString())); + } + } + } + + @Override + public ScopedContext.Instance newScopedContext() { + return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + } + + /** + * 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. + */ + @Override + public ScopedContext.Instance newScopedContext(final String key, final Object value) { + if (value != null) { + final Instance parent = getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + return new Instance(parent, key, value); + } else { + if (getContext().isPresent()) { + final Map map = getContextMap(); + map.remove(key); + return new Instance(this, map); + } + } + return newScopedContext(); + } + + /** + * Creates a ScopedContext Instance with a Map of keys and values. + * @param map the Map. + * @return the ScopedContext Instance constructed. + */ + @Override + public ScopedContext.Instance newScopedContext(final Map map) { + if (map != null && !map.isEmpty()) { + final Map objectMap = new HashMap<>(); + if (getContext().isPresent()) { + objectMap.putAll(getContext().get().contextMap); + } + map.forEach((key, value) -> { + if (value == null || (value instanceof String && ((String) value).isEmpty())) { + objectMap.remove(key); + } else { + objectMap.put(key, value); + } + }); + return new Instance(this, objectMap); + } else { + return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; + } + } + + private static void setupContext( + final Map contextMap, + final Map threadContextMap, + final Collection contextStack, + final Instance context) { + 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(context.getProvider(), contextMap); + } + if (threadContextMap != null && !threadContextMap.isEmpty()) { + ThreadContext.putAll(threadContextMap); + } + if (contextStack != null) { + ThreadContext.setStack(contextStack); + } + context.getProvider().addScopedContext(scopedContext); + } + + private static final class Instance implements ScopedContext.Instance { + + private final QueuedScopedContextProvider provider; + private final Instance parent; + private final String key; + private final Object value; + private final Map contextMap; + + private Instance(final QueuedScopedContextProvider provider) { + this.provider = provider; + parent = null; + key = null; + value = null; + contextMap = null; + } + + private Instance(final QueuedScopedContextProvider provider, final Map map) { + this.provider = provider; + parent = null; + key = null; + value = null; + contextMap = map; + } + + private Instance(final Instance parent, final String key, final Object value) { + provider = parent.getProvider(); + this.parent = parent; + this.key = key; + this.value = value; + 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. + */ + @Override + public Instance where(final String key, final 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. + */ + @Override + public Instance where(final String key, final Supplier supplier) { + return addObject(key, supplier.get()); + } + + private Instance addObject(final String key, final Object obj) { + return obj != null ? new Instance(this, key, obj) : this; + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * + * @param task the code block to execute. + */ + @Override + public void run(final Runnable task) { + new Runner(this, null, null, task).run(); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + @Override + public Future run(final ExecutorService executorService, final Runnable task) { + return executorService.submit( + new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task), null); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * + * @param task the code block to execute. + * @return the return value from the code block. + */ + @Override + public R call(final Callable task) throws Exception { + return new Caller<>(this, null, null, task).call(); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + @Override + public Future call(final ExecutorService executorService, final Callable task) { + return executorService.submit( + new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task)); + } + + private QueuedScopedContextProvider getProvider() { + return provider; + } + } + + 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( + final Instance context, + final Map threadContextMap, + final ThreadContext.ContextStack contextStack, + final Runnable op) { + this.context = context; + this.threadContextMap = threadContextMap; + this.contextStack = contextStack; + this.op = op; + } + + @Override + public void run() { + setupContext(contextMap, threadContextMap, contextStack, context); + try { + op.run(); + } finally { + context.getProvider().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( + final Instance context, + final Map threadContextMap, + final ThreadContext.ContextStack contextStack, + final Callable op) { + this.context = context; + this.threadContextMap = threadContextMap; + this.contextStack = contextStack; + this.op = op; + } + + @Override + public R call() throws Exception { + setupContext(contextMap, threadContextMap, contextStack, context); + try { + return op.call(); + } finally { + context.getProvider().removeScopedContext(); + ThreadContext.clearAll(); + } + } + } +} From ebc3aafd296b0772aea0262ce741267a712b6655 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Sat, 6 Apr 2024 00:00:51 -0700 Subject: [PATCH 17/19] Return raw object as it was stored. Add wrap methods --- .../logging/log4j/ResourceLoggerTest.java | 2 - .../logging/log4j/ScopedContextTest.java | 152 +++++++++++------- .../apache/logging/log4j/ScopedContext.java | 23 ++- .../DefaultScopedContextProvider.java | 22 +++ .../internal/QueuedScopedContextProvider.java | 22 +++ 5 files changed, 158 insertions(+), 63 deletions(-) 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 c2273e27907..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 @@ -29,13 +29,11 @@ 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.Disabled; import org.junit.jupiter.api.Test; /** * Class Description goes here. */ -@Disabled("Does not work with the NO-OP implementation of ScopedContextProvider in the API.") public class ResourceLoggerTest { @BeforeAll public static void beforeAll() { 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 index d9ba5872e62..40e0c8deacd 100644 --- 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 @@ -65,46 +65,54 @@ public void testRunWhere() { 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); + try { + final long id = Thread.currentThread().getId(); + final AtomicLong counter = new AtomicLong(0); + ScopedContext.runWhere("key1", "Log4j2", () -> { assertThat(ScopedContext.get("key1"), equalTo("Log4j2")); - counter.incrementAndGet(); + 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()); + } }); - try { - future.get(); - assertTrue(future.isDone()); - assertEquals(1, counter.get()); - } catch (Exception ex) { - fail("Failed with " + ex.getMessage()); - } - }); + } finally { + executorService.shutdown(); + } } @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); + try { + final long id = Thread.currentThread().getId(); + final AtomicLong counter = new AtomicLong(0); + ScopedContext.where("key1", "Log4j2").run(() -> { assertThat(ScopedContext.get("key1"), equalTo("Log4j2")); - counter.incrementAndGet(); + 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()); + } }); - try { - future.get(); - assertTrue(future.isDone()); - assertEquals(1, counter.get()); - } catch (Exception ex) { - fail("Failed with " + ex.getMessage()); - } - }); + } finally { + executorService.shutdown(); + } } @Test @@ -112,43 +120,75 @@ 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 { + 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"); }); - 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"); - }); + } finally { + executorService.shutdown(); + } } @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); + try { + final long id = Thread.currentThread().getId(); + final AtomicInteger counter = new AtomicInteger(0); + int returnVal = ScopedContext.callWhere("key1", "Log4j2", () -> { assertThat(ScopedContext.get("key1"), equalTo("Log4j2")); - return counter.incrementAndGet(); + 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)); + } finally { + executorService.shutdown(); + } + } + + @Test + public void testAsyncCall() throws Exception { + BlockingQueue workQueue = new ArrayBlockingQueue<>(5); + ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, workQueue); + try { + final long id = Thread.currentThread().getId(); + final AtomicInteger counter = new AtomicInteger(0); + ScopedContext.Instance instance = + ScopedContext.where("key1", "Log4j2").where("key2", "value2"); + Future future = executorService.submit(instance.wrap(() -> { + assertThat(ScopedContext.get("key1"), equalTo("Log4j2")); + assertNotEquals(Thread.currentThread().getId(), id); + assertThat(ScopedContext.get("key2"), equalTo("value2")); + return counter.incrementAndGet(); + })); Integer val = future.get(); assertTrue(future.isDone()); assertEquals(1, counter.get()); - return val; - }); - assertThat(returnVal, equalTo(1)); + assertThat(val, equalTo(1)); + } finally { + executorService.shutdown(); + } } } 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 37cb26e0940..8f0b262b1e9 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 @@ -151,12 +151,9 @@ public static R callWhere(final Map map, final Callable task) * Return the object with the specified key from the current context. * @param key the key. * @return the value of the key or null. - * @param The type of object expected. - * @throws ClassCastException if the specified type does not match the object stored. */ - @SuppressWarnings("unchecked") - public static T get(String key) { - return (T) provider.getValue(key); + public static Object get(String key) { + return provider.getValue(key); } /** @@ -221,5 +218,21 @@ public interface Instance { * @return a Future representing pending completion of the task */ Future call(ExecutorService executorService, Callable task); + + /** + * Wraps the provided Runnable method with a Runnable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's run method is called. + * @param task the Runnable task to perform. + * @return a Runnable. + */ + Runnable wrap(Runnable task); + + /** + * Wraps the provided Callable method with a Callable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's call method is called. + * @param task the Callable task to perform. + * @return a Callable. + */ + Callable wrap(Callable task); } } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java index fd5417f1673..018a5bc8b17 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java @@ -332,6 +332,28 @@ public Future call(final ExecutorService executorService, final Callable< new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task)); } + /** + * Wraps the provided Runnable method with a Runnable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's run method is called. + * @param task the Runnable task to perform. + * @return a Runnable. + */ + @Override + public Runnable wrap(Runnable task) { + return new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); + } + + /** + * Wraps the provided Callable method with a Callable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's call method is called. + * @param task the Callable task to perform. + * @return a Callable. + */ + @Override + public Callable wrap(Callable task) { + return new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); + } + private DefaultScopedContextProvider getProvider() { return provider; } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java index d5452135b56..cbba574699a 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java @@ -322,6 +322,28 @@ public Future call(final ExecutorService executorService, final Callable< new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task)); } + /** + * Wraps the provided Runnable method with a Runnable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's run method is called. + * @param task the Runnable task to perform. + * @return a Runnable. + */ + @Override + public Runnable wrap(Runnable task) { + return new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); + } + + /** + * Wraps the provided Callable method with a Callable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's call method is called. + * @param task the Callable task to perform. + * @return a Callable. + */ + @Override + public Callable wrap(Callable task) { + return new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); + } + private QueuedScopedContextProvider getProvider() { return provider; } From 132c2d33e4496cabd05a1e6f331644b5494b771b Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Thu, 11 Apr 2024 09:40:17 -0700 Subject: [PATCH 18/19] More abstractions --- .../logging/log4j/ScopedContextTest.java | 49 ++ .../apache/logging/log4j/ScopedContext.java | 58 ++ .../spi/AbstractScopedContextProvider.java | 527 ++++++++++++++++++ .../log4j/spi/ScopedContextProvider.java | 7 + .../DefaultScopedContextProvider.java | 385 +------------ .../internal/QueuedScopedContextProvider.java | 357 +----------- 6 files changed, 663 insertions(+), 720 deletions(-) create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java 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 index 40e0c8deacd..ac938f85fe3 100644 --- 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 @@ -18,6 +18,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -168,6 +169,54 @@ public void testThreadCall() throws Exception { } } + @Test + public void testThreadContext() throws Exception { + ThreadContext.put("Dog", "Fido"); + ScopedContext.runWhere("key1", "Log4j2", () -> { + assertThat(ScopedContext.get("key1"), equalTo("Log4j2")); + assertThat(ThreadContext.get("Dog"), equalTo("Fido")); + }); + + BlockingQueue workQueue = new ArrayBlockingQueue<>(5); + ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, workQueue); + try { + 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.withThreadContext() + .callWhere("key2", "value2", executorService, () -> { + assertNotEquals(Thread.currentThread().getId(), id); + assertThat(ScopedContext.get("key1"), equalTo("Log4j2")); + assertThat(ThreadContext.get("Dog"), equalTo("Fido")); + return counter.incrementAndGet(); + }); + Integer val = future.get(); + assertTrue(future.isDone()); + assertEquals(1, counter.get()); + return val; + }); + assertThat(returnVal, equalTo(1)); + 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")); + assertThat(ThreadContext.get("Dog"), nullValue()); + return counter.incrementAndGet(); + }); + Integer val = future.get(); + assertTrue(future.isDone()); + assertEquals(2, counter.get()); + return val; + }); + assertThat(returnVal, equalTo(1)); + assertThat(ThreadContext.get("Dog"), equalTo("Fido")); + } finally { + executorService.shutdown(); + } + } + @Test public void testAsyncCall() throws Exception { BlockingQueue workQueue = new ArrayBlockingQueue<>(5); 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 8f0b262b1e9..0a39634eddf 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 @@ -85,6 +85,10 @@ public static Instance where(final Map map) { return provider.newScopedContext(map); } + public static Instance withThreadContext() { + return provider.newScopedContext(true); + } + /** * Creates a ScopedContext with a single key/value pair and calls a method. * @param key the key. @@ -188,6 +192,13 @@ public interface Instance { */ Instance where(String key, Supplier supplier); + /** + * Creates an Instance to declare the ThreadContext should be included. + * + * @return the ScopedContext being constructed. + */ + Instance withThreadContext(); + /** * Executes a code block that includes all the key/value pairs added to the ScopedContext. * @@ -198,11 +209,34 @@ public interface Instance { /** * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. * + * @param executorService The ExecutorService to use. * @param task the code block to execute. * @return a Future representing pending completion of the task */ Future run(ExecutorService executorService, Runnable task); + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param value the value associated with the key. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + Future runWhere(String key, Object value, ExecutorService executorService, Runnable task); + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param supplier the function to generate the value. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + Future runWhere(String key, Supplier supplier, ExecutorService executorService, Runnable task); + /** * Executes a code block that includes all the key/value pairs added to the ScopedContext. * @@ -214,11 +248,35 @@ public interface Instance { /** * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. * + * @param executorService The ExecutorService to use. * @param task the code block to execute. * @return a Future representing pending completion of the task */ Future call(ExecutorService executorService, Callable task); + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param value the value associated with the key. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + Future callWhere(String key, Object value, ExecutorService executorService, Callable task); + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param supplier the function to generate the value. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + Future callWhere( + String key, Supplier supplier, ExecutorService executorService, Callable task); + /** * Wraps the provided Runnable method with a Runnable method that will instantiate the Scoped and Thread * Contexts in the target Thread before the caller's run method is called. diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java new file mode 100644 index 00000000000..ecc777ad920 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractScopedContextProvider.java @@ -0,0 +1,527 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ScopedContext; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.StringMap; + +/** + * An implementation of {@link ScopedContextProvider} that uses the simplest implementation. + * @since 2.24.0 + */ +public abstract class AbstractScopedContextProvider implements ScopedContextProvider { + + public static final Logger LOGGER = StatusLogger.getLogger(); + + /** + * Returns an immutable Instance. + * @return The current context Instance. + */ + protected abstract Optional getContext(); + + /** + * Add the ScopeContext. + * @param context The ScopeContext. + */ + protected abstract void addScopedContext(final MapInstance context); + + /** + * Remove the top ScopeContext. + */ + protected abstract void removeScopedContext(); + + @Override + public Map getContextMap() { + final Optional context = getContext(); + if (context.isPresent()) { + return ((MapInstance) context.get()).contextMap; + } + return Collections.emptyMap(); + } + + /** + * 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. + */ + @Override + public Object getValue(final String key) { + return getContextMap().get(key); + } + + /** + * 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. + */ + @Override + public String getString(final String key) { + final Object obj = getValue(key); + return obj != null ? obj.toString() : null; + } + + /** + * Adds all the String rendered objects in the context map to the provided Map. + * @param map The Map to add entries to. + */ + @Override + public void addContextMapTo(final StringMap map) { + final Optional context = getContext(); + if (context.isPresent()) { + final Map contextMap = ((MapInstance) context.get()).contextMap; + if (contextMap != null && !contextMap.isEmpty()) { + contextMap.forEach((key, value) -> map.putValue(key, value.toString())); + } + } + } + + @Override + public ScopedContext.Instance newScopedContext() { + return getContext().isPresent() ? getContext().get() : 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. + */ + @Override + public ScopedContext.Instance newScopedContext(final String key, final Object value) { + Optional context = getContext(); + final MapInstance parent = (MapInstance) context.orElse(null); + final boolean withThreadContext = parent != null && parent.withThreadContext; + return newKeyValueInstance(parent, key, value, withThreadContext); + } + + /** + * Creates a ScopedContext Instance with a Map of keys and values. + * @param map the Map. + * @return the ScopedContext Instance constructed. + */ + @Override + public ScopedContext.Instance newScopedContext(final Map map) { + final Map objectMap = new HashMap<>(getContextMap()); + if (map != null && !map.isEmpty()) { + map.forEach((key, value) -> { + if (value == null || (value instanceof String && ((String) value).isEmpty())) { + objectMap.remove(key); + } else { + objectMap.put(key, value); + } + }); + } + return newMapInstance(null, objectMap, false); + } + + /** + * Creates a ScopedContext Instance that indicates the ThreadContext should be included. + * @param withThreadContext true if the ThreadContext should be included. + * @return the ScopedContext Instance constructed. + */ + @Override + public ScopedContext.Instance newScopedContext(final boolean withThreadContext) { + MapInstance parent = (MapInstance) getContext().orElse(null); + return newInstance(parent, withThreadContext); + } + + protected KeyValueInstance newKeyValueInstance( + Instance instance, String key, Object value, boolean withThreadContext) { + return new KeyValueInstance(this, instance, key, value, withThreadContext); + } + + protected Instance newInstance(Instance instance, boolean withThreadContext) { + return new Instance(this, instance, withThreadContext); + } + + protected MapInstance newMapInstance( + final Instance instance, final Map map, final Boolean withThreadContext) { + final Map objectMap = new HashMap<>(getContextMap()); + Instance parent = instance; + while (parent != null && !(parent instanceof MapInstance)) { + if (parent instanceof KeyValueInstance) { + objectMap.put(((KeyValueInstance) parent).key, ((KeyValueInstance) parent).value); + } + parent = parent.getParent(); + } + map.forEach((key, value) -> { + if (value == null || (value instanceof String && ((String) value).isEmpty())) { + objectMap.remove(key); + } else { + objectMap.put(key, value); + } + }); + return new MapInstance(this, instance, (MapInstance) getContext().orElse(null), objectMap, withThreadContext); + } + + /** + * When an Instance is created it may contain a Key/Value pair from a Where method. When a run method is + * called a Map will be created and all the Key/Value pairs from parent instances will be added to + * it along with all the keys from the first Instance encountered that contains a Map. In this + * way we avoid continually copying Maps in order to add a single Key/Value pair. + */ + protected static class Instance implements ScopedContext.Instance { + + protected final AbstractScopedContextProvider provider; + protected final Instance parent; + protected final boolean withThreadContext; + + protected boolean isWithThreadContext() { + return withThreadContext; + } + + private Instance( + final AbstractScopedContextProvider provider, final Instance parent, final boolean withThreadContext) { + this.provider = provider != null ? provider : parent.provider; + this.parent = parent; + this.withThreadContext = withThreadContext; + } + + public AbstractScopedContextProvider getProvider() { + return provider; + } + + public Instance getParent() { + return parent; + } + + /** + * 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. + */ + @Override + public Instance where(final String key, final 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. + */ + @Override + public Instance where(final String key, final Supplier supplier) { + return addObject(key, supplier.get()); + } + + /** + * Creates an Instance to declare the ThreadContext should be included. + * + * @return the ScopedContext being constructed. + */ + @Override + public Instance withThreadContext() { + return provider.newInstance(this, true); + } + + protected Instance addObject(final String key, final Object obj) { + return obj != null ? provider.newKeyValueInstance(this, key, obj, this.withThreadContext) : this; + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param value the value associated with the key. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + */ + @Override + public Future runWhere(String key, Object value, ExecutorService executorService, Runnable task) { + Map map = this.withThreadContext ? ThreadContext.getContext() : null; + Instance instance = addObject(key, value); + final MapInstance context = provider.newMapInstance(instance, new HashMap<>(), this.withThreadContext); + return executorService.submit(new Runner(context, map, ThreadContext.getImmutableStack(), task), null); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param supplier the function to generate the value. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + */ + @Override + public Future runWhere( + String key, Supplier supplier, ExecutorService executorService, Runnable task) { + return runWhere(key, supplier.get(), executorService, task); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param value the value associated with the key. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + */ + @Override + public Future callWhere(String key, Object value, ExecutorService executorService, Callable task) { + Map map = this.withThreadContext ? ThreadContext.getContext() : null; + ThreadContext.ContextStack stack = withThreadContext ? ThreadContext.getImmutableStack() : null; + Instance instance = addObject(key, value); + final MapInstance context = provider.newMapInstance(instance, new HashMap<>(), this.withThreadContext); + return executorService.submit(new Caller<>(context, map, stack, task)); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param key the key to add. + * @param supplier the function to generate the value. + * @param executorService The ExecutorService to use. + * @param task the code block to execute. + */ + @Override + public Future callWhere( + String key, Supplier supplier, ExecutorService executorService, Callable task) { + return callWhere(key, supplier.get(), executorService, task); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * + * @param task the code block to execute. + */ + @Override + public void run(final Runnable task) { + final MapInstance context = this instanceof MapInstance + ? (MapInstance) this + : provider.newMapInstance(this, new HashMap<>(), this.withThreadContext); + new Runner(context, null, null, task).run(); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + @Override + public Future run(final ExecutorService executorService, final Runnable task) { + Map map = this.withThreadContext ? ThreadContext.getContext() : null; + final MapInstance context = this instanceof MapInstance + ? (MapInstance) this + : provider.newMapInstance(this, new HashMap<>(), this.withThreadContext); + return executorService.submit(new Runner(context, map, ThreadContext.getImmutableStack(), task), null); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext. + * + * @param task the code block to execute. + * @return the return value from the code block. + */ + @Override + public R call(final Callable task) throws Exception { + final MapInstance context = this instanceof MapInstance + ? (MapInstance) this + : provider.newMapInstance(this, new HashMap<>(), this.withThreadContext); + return new Caller<>(context, null, null, task).call(); + } + + /** + * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. + * + * @param task the code block to execute. + * @return a Future representing pending completion of the task + */ + @Override + public Future call(final ExecutorService executorService, final Callable task) { + Map map = this.withThreadContext ? ThreadContext.getContext() : null; + ThreadContext.ContextStack stack = withThreadContext ? ThreadContext.getImmutableStack() : null; + final MapInstance context = this instanceof MapInstance + ? (MapInstance) this + : provider.newMapInstance(this, new HashMap<>(), this.withThreadContext); + return executorService.submit(new Caller<>(context, map, stack, task)); + } + + /** + * Wraps the provided Runnable method with a Runnable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's run method is called. + * @param task the Runnable task to perform. + * @return a Runnable. + */ + @Override + public Runnable wrap(Runnable task) { + Map map = this.withThreadContext ? ThreadContext.getContext() : null; + ThreadContext.ContextStack stack = withThreadContext ? ThreadContext.getImmutableStack() : null; + final MapInstance context = this instanceof MapInstance + ? (MapInstance) this + : provider.newMapInstance(this, new HashMap<>(), this.withThreadContext); + return new Runner(context, map, stack, task); + } + + /** + * Wraps the provided Callable method with a Callable method that will instantiate the Scoped and Thread + * Contexts in the target Thread before the caller's call method is called. + * @param task the Callable task to perform. + * @return a Callable. + */ + @Override + public Callable wrap(Callable task) { + Map map = this.withThreadContext ? ThreadContext.getContext() : null; + ThreadContext.ContextStack stack = withThreadContext ? ThreadContext.getImmutableStack() : null; + final MapInstance context = this instanceof MapInstance + ? (MapInstance) this + : provider.newMapInstance(this, new HashMap<>(), this.withThreadContext); + return new Caller<>(context, map, stack, task); + } + } + + protected static class MapInstance extends Instance { + private final Map contextMap; + private final MapInstance previous; + + public MapInstance( + final AbstractScopedContextProvider provider, + final Instance parent, + final MapInstance previous, + final Map map, + Boolean withThreadContext) { + super(provider, parent, withThreadContext); + this.contextMap = map; + this.previous = previous; + } + + public MapInstance getPrevious() { + return previous; + } + } + + /** + * + */ + protected static class KeyValueInstance extends Instance { + protected final String key; + protected final Object value; + + public KeyValueInstance( + final AbstractScopedContextProvider provider, + final Instance parent, + final String key, + final Object value, + Boolean withThreadContext) { + super(provider, parent, withThreadContext); + this.key = key; + this.value = value; + } + } + + protected abstract static class AbstractWorker { + private final Map contextMap = new HashMap<>(); + private final Map threadContextMap; + private final ThreadContext.ContextStack contextStack; + private final MapInstance context; + + protected AbstractWorker( + final MapInstance context, + final Map threadContextMap, + final ThreadContext.ContextStack contextStack) { + this.context = context; + this.threadContextMap = threadContextMap; + this.contextStack = contextStack; + } + + protected void setupContext() { + if (threadContextMap != null) { + ThreadContext.clearMap(); + ThreadContext.putAll(threadContextMap); + } + if (contextStack != null) { + ThreadContext.clearStack(); + ThreadContext.setStack(contextStack); + } + context.getProvider().addScopedContext(context); + } + + protected void restoreContext() { + context.getProvider().removeScopedContext(); + if (threadContextMap != null) { + ThreadContext.clearMap(); + } + if (contextStack != null) { + ThreadContext.clearStack(); + } + } + } + + private static class Runner extends AbstractWorker implements Runnable { + private final Runnable op; + + public Runner( + final MapInstance context, + final Map threadContextMap, + final ThreadContext.ContextStack contextStack, + final Runnable op) { + super(context, threadContextMap, contextStack); + this.op = op; + } + + @Override + public void run() { + setupContext(); + try { + op.run(); + } finally { + restoreContext(); + } + } + } + + private static class Caller extends AbstractWorker implements Callable { + private final Callable op; + + public Caller( + final MapInstance context, + final Map threadContextMap, + final ThreadContext.ContextStack contextStack, + final Callable op) { + super(context, threadContextMap, contextStack); + this.op = op; + } + + @Override + public R call() throws Exception { + setupContext(); + try { + return op.call(); + } finally { + restoreContext(); + } + } + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java index 5c14f036d95..53755ccb212 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/ScopedContextProvider.java @@ -78,4 +78,11 @@ default void addContextMapTo(final StringMap map) { * @return A new instance of a scoped context. */ ScopedContext.Instance newScopedContext(Map map); + + /** + * Creates a new context indicating that the ThreadContext should be included. + * @param withThreadContext true if the ThreadContext should be included. + * @return A new instance of a scoped context. + */ + ScopedContext.Instance newScopedContext(boolean withThreadContext); } diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java index 018a5bc8b17..76fa0a3b33b 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/internal/DefaultScopedContextProvider.java @@ -16,38 +16,25 @@ */ package org.apache.logging.log4j.spi.internal; -import java.util.Collection; -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.ExecutorService; -import java.util.concurrent.Future; -import java.util.function.Supplier; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.ScopedContext; -import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.spi.AbstractScopedContextProvider; import org.apache.logging.log4j.spi.ScopedContextProvider; -import org.apache.logging.log4j.status.StatusLogger; -import org.apache.logging.log4j.util.StringMap; /** * An implementation of {@link ScopedContextProvider} that uses the simplest implementation. * @since 2.24.0 */ -public class DefaultScopedContextProvider implements ScopedContextProvider { +public class DefaultScopedContextProvider extends AbstractScopedContextProvider { - public static final Logger LOGGER = StatusLogger.getLogger(); public static final ScopedContextProvider INSTANCE = new DefaultScopedContextProvider(); - private final ThreadLocal scopedContext = new ThreadLocal<>(); + private final ThreadLocal scopedContext = new ThreadLocal<>(); /** * Returns an immutable Map containing all the key/value pairs as Object objects. * @return The current context Instance. */ - private Optional getContext() { + protected Optional getContext() { return Optional.ofNullable(scopedContext.get()); } @@ -55,367 +42,23 @@ private Optional getContext() { * Add the ScopeContext. * @param context The ScopeContext. */ - private void addScopedContext(final Instance context) { - Instance current = scopedContext.get(); + protected void addScopedContext(final MapInstance context) { scopedContext.set(context); } /** * Remove the top ScopeContext. */ - private void removeScopedContext() { - final Instance current = scopedContext.get(); - if (current != null) { - if (current.parent != null) { - scopedContext.set(current.parent); - } else { - scopedContext.remove(); - } - } - } - - @Override - public Map getContextMap() { - final Optional context = getContext(); - return context.isPresent() - && context.get().contextMap != null - && !context.get().contextMap.isEmpty() - ? Collections.unmodifiableMap(context.get().contextMap) - : Collections.emptyMap(); - } - - /** - * 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. - */ - @Override - public Object getValue(final String key) { - final Optional context = getContext(); - return context.map(instance -> instance.contextMap) - .map(map -> map.get(key)) - .orElse(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. - */ - @Override - public String getString(final String key) { - final Optional context = getContext(); - if (context.isPresent()) { - final Object obj = context.get().contextMap.get(key); - if (obj != null) { - return obj.toString(); - } - } - return null; - } - - /** - * Adds all the String rendered objects in the context map to the provided Map. - * @param map The Map to add entries to. - */ - @Override - public void addContextMapTo(final StringMap map) { - final Optional context = getContext(); - if (context.isPresent()) { - final Map contextMap = context.get().contextMap; - if (contextMap != null && !contextMap.isEmpty()) { - contextMap.forEach((key, value) -> map.putValue(key, value.toString())); - } - } - } - - @Override - public ScopedContext.Instance newScopedContext() { - return getContext().isPresent() ? getContext().get() : 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. - */ - @Override - public ScopedContext.Instance newScopedContext(final String key, final Object value) { - if (value != null) { - final Instance parent = getContext().isPresent() ? getContext().get() : null; - if (parent != null) { - return new Instance(parent, key, value); - } else { - return new Instance(this, key, value); - } - } else { - if (getContext().isPresent()) { - final Map map = getContextMap(); - map.remove(key); - return new Instance(this, map); - } - } - return newScopedContext(); - } - - /** - * Creates a ScopedContext Instance with a Map of keys and values. - * @param map the Map. - * @return the ScopedContext Instance constructed. - */ - @Override - public ScopedContext.Instance newScopedContext(final Map map) { - if (map != null && !map.isEmpty()) { - final Map objectMap = new HashMap<>(); - if (getContext().isPresent()) { - objectMap.putAll(getContext().get().contextMap); - } - map.forEach((key, value) -> { - if (value == null || (value instanceof String && ((String) value).isEmpty())) { - objectMap.remove(key); - } else { - objectMap.put(key, value); - } - }); - return new Instance(this, objectMap); + protected void removeScopedContext() { + MapInstance current = scopedContext.get(); + if (current == null) { + return; + } + MapInstance previous = current.getPrevious(); + if (previous != null) { + scopedContext.set(previous); } else { - return getContext().isPresent() ? getContext().get() : null; - } - } - - private static void setupContext( - final Map contextMap, - final Map threadContextMap, - final Collection contextStack, - final Instance context) { - 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(context.getProvider(), contextMap); - } - if (threadContextMap != null && !threadContextMap.isEmpty()) { - ThreadContext.putAll(threadContextMap); - } - if (contextStack != null) { - ThreadContext.setStack(contextStack); - } - context.getProvider().addScopedContext(scopedContext); - } - - private static final class Instance implements ScopedContext.Instance { - - private final DefaultScopedContextProvider provider; - private final Instance parent; - private final String key; - private final Object value; - private final Map contextMap; - - private Instance(final DefaultScopedContextProvider provider) { - this.provider = provider; - parent = null; - key = null; - value = null; - contextMap = null; - } - - private Instance(final DefaultScopedContextProvider provider, final Map map) { - this.provider = provider; - parent = null; - key = null; - value = null; - contextMap = map; - } - - private Instance(final DefaultScopedContextProvider provider, final String key, final Object value) { - this.provider = provider; - parent = null; - this.key = key; - this.value = value; - contextMap = null; - } - - private Instance(final Instance parent, final String key, final Object value) { - provider = parent.getProvider(); - this.parent = parent; - this.key = key; - this.value = value; - contextMap = null; - } - - public Instance getParent() { - return parent; - } - - /** - * 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. - */ - @Override - public Instance where(final String key, final 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. - */ - @Override - public Instance where(final String key, final Supplier supplier) { - return addObject(key, supplier.get()); - } - - private Instance addObject(final String key, final Object obj) { - return obj != null ? new Instance(this, key, obj) : this; - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext. - * - * @param task the code block to execute. - */ - @Override - public void run(final Runnable task) { - new Runner(this, null, null, task).run(); - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. - * - * @param task the code block to execute. - * @return a Future representing pending completion of the task - */ - @Override - public Future run(final ExecutorService executorService, final Runnable task) { - return executorService.submit( - new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task), null); - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext. - * - * @param task the code block to execute. - * @return the return value from the code block. - */ - @Override - public R call(final Callable task) throws Exception { - return new Caller<>(this, null, null, task).call(); - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. - * - * @param task the code block to execute. - * @return a Future representing pending completion of the task - */ - @Override - public Future call(final ExecutorService executorService, final Callable task) { - return executorService.submit( - new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task)); - } - - /** - * Wraps the provided Runnable method with a Runnable method that will instantiate the Scoped and Thread - * Contexts in the target Thread before the caller's run method is called. - * @param task the Runnable task to perform. - * @return a Runnable. - */ - @Override - public Runnable wrap(Runnable task) { - return new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); - } - - /** - * Wraps the provided Callable method with a Callable method that will instantiate the Scoped and Thread - * Contexts in the target Thread before the caller's call method is called. - * @param task the Callable task to perform. - * @return a Callable. - */ - @Override - public Callable wrap(Callable task) { - return new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); - } - - private DefaultScopedContextProvider getProvider() { - return provider; - } - } - - 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( - final Instance context, - final Map threadContextMap, - final ThreadContext.ContextStack contextStack, - final Runnable op) { - this.context = context; - this.threadContextMap = threadContextMap; - this.contextStack = contextStack; - this.op = op; - } - - @Override - public void run() { - setupContext(contextMap, threadContextMap, contextStack, context); - try { - op.run(); - } finally { - context.getProvider().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( - final Instance context, - final Map threadContextMap, - final ThreadContext.ContextStack contextStack, - final Callable op) { - this.context = context; - this.threadContextMap = threadContextMap; - this.contextStack = contextStack; - this.op = op; - } - - @Override - public R call() throws Exception { - setupContext(contextMap, threadContextMap, contextStack, context); - try { - return op.call(); - } finally { - context.getProvider().removeScopedContext(); - ThreadContext.clearAll(); - } + scopedContext.remove(); } } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java index cbba574699a..626b9c02553 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/internal/QueuedScopedContextProvider.java @@ -17,37 +17,23 @@ package org.apache.logging.log4j.core.impl.internal; import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Collections; import java.util.Deque; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.function.Supplier; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.ScopedContext; -import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.spi.AbstractScopedContextProvider; import org.apache.logging.log4j.spi.ScopedContextProvider; -import org.apache.logging.log4j.status.StatusLogger; -import org.apache.logging.log4j.util.StringMap; -public class QueuedScopedContextProvider implements ScopedContextProvider { +public class QueuedScopedContextProvider extends AbstractScopedContextProvider { - public static final Logger LOGGER = StatusLogger.getLogger(); public static final ScopedContextProvider INSTANCE = new QueuedScopedContextProvider(); private final ThreadLocal> scopedContext = new ThreadLocal<>(); - private final Instance EMPTY_INSTANCE = new Instance(this); - /** * 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 Optional getContext() { + @Override + protected Optional getContext() { final Deque stack = scopedContext.get(); return stack != null ? Optional.of(stack.getFirst()) : Optional.empty(); } @@ -56,7 +42,8 @@ private Optional getContext() { * Add the ScopeContext. * @param context The ScopeContext. */ - private void addScopedContext(final Instance context) { + @Override + protected void addScopedContext(final MapInstance context) { Deque stack = scopedContext.get(); if (stack == null) { stack = new ArrayDeque<>(); @@ -68,7 +55,8 @@ private void addScopedContext(final Instance context) { /** * Remove the top ScopeContext. */ - private void removeScopedContext() { + @Override + protected void removeScopedContext() { final Deque stack = scopedContext.get(); if (stack != null) { if (!stack.isEmpty()) { @@ -79,333 +67,4 @@ private void removeScopedContext() { } } } - - @Override - public Map getContextMap() { - final Optional context = getContext(); - return context.isPresent() - && context.get().contextMap != null - && !context.get().contextMap.isEmpty() - ? Collections.unmodifiableMap(context.get().contextMap) - : Collections.emptyMap(); - } - - /** - * 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. - */ - @Override - public Object getValue(final String key) { - final Optional context = getContext(); - return context.map(instance -> instance.contextMap) - .map(map -> map.get(key)) - .orElse(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. - */ - @Override - public String getString(final String key) { - final Optional context = getContext(); - if (context.isPresent()) { - final Object obj = context.get().contextMap.get(key); - if (obj != null) { - return obj.toString(); - } - } - return null; - } - - /** - * Adds all the String rendered objects in the context map to the provided Map. - * @param map The Map to add entries to. - */ - @Override - public void addContextMapTo(final StringMap map) { - final Optional context = getContext(); - if (context.isPresent()) { - final Map contextMap = context.get().contextMap; - if (contextMap != null && !contextMap.isEmpty()) { - contextMap.forEach((key, value) -> map.putValue(key, value.toString())); - } - } - } - - @Override - public ScopedContext.Instance newScopedContext() { - return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; - } - - /** - * 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. - */ - @Override - public ScopedContext.Instance newScopedContext(final String key, final Object value) { - if (value != null) { - final Instance parent = getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; - return new Instance(parent, key, value); - } else { - if (getContext().isPresent()) { - final Map map = getContextMap(); - map.remove(key); - return new Instance(this, map); - } - } - return newScopedContext(); - } - - /** - * Creates a ScopedContext Instance with a Map of keys and values. - * @param map the Map. - * @return the ScopedContext Instance constructed. - */ - @Override - public ScopedContext.Instance newScopedContext(final Map map) { - if (map != null && !map.isEmpty()) { - final Map objectMap = new HashMap<>(); - if (getContext().isPresent()) { - objectMap.putAll(getContext().get().contextMap); - } - map.forEach((key, value) -> { - if (value == null || (value instanceof String && ((String) value).isEmpty())) { - objectMap.remove(key); - } else { - objectMap.put(key, value); - } - }); - return new Instance(this, objectMap); - } else { - return getContext().isPresent() ? getContext().get() : EMPTY_INSTANCE; - } - } - - private static void setupContext( - final Map contextMap, - final Map threadContextMap, - final Collection contextStack, - final Instance context) { - 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(context.getProvider(), contextMap); - } - if (threadContextMap != null && !threadContextMap.isEmpty()) { - ThreadContext.putAll(threadContextMap); - } - if (contextStack != null) { - ThreadContext.setStack(contextStack); - } - context.getProvider().addScopedContext(scopedContext); - } - - private static final class Instance implements ScopedContext.Instance { - - private final QueuedScopedContextProvider provider; - private final Instance parent; - private final String key; - private final Object value; - private final Map contextMap; - - private Instance(final QueuedScopedContextProvider provider) { - this.provider = provider; - parent = null; - key = null; - value = null; - contextMap = null; - } - - private Instance(final QueuedScopedContextProvider provider, final Map map) { - this.provider = provider; - parent = null; - key = null; - value = null; - contextMap = map; - } - - private Instance(final Instance parent, final String key, final Object value) { - provider = parent.getProvider(); - this.parent = parent; - this.key = key; - this.value = value; - 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. - */ - @Override - public Instance where(final String key, final 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. - */ - @Override - public Instance where(final String key, final Supplier supplier) { - return addObject(key, supplier.get()); - } - - private Instance addObject(final String key, final Object obj) { - return obj != null ? new Instance(this, key, obj) : this; - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext. - * - * @param task the code block to execute. - */ - @Override - public void run(final Runnable task) { - new Runner(this, null, null, task).run(); - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. - * - * @param task the code block to execute. - * @return a Future representing pending completion of the task - */ - @Override - public Future run(final ExecutorService executorService, final Runnable task) { - return executorService.submit( - new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task), null); - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext. - * - * @param task the code block to execute. - * @return the return value from the code block. - */ - @Override - public R call(final Callable task) throws Exception { - return new Caller<>(this, null, null, task).call(); - } - - /** - * Executes a code block that includes all the key/value pairs added to the ScopedContext on a different Thread. - * - * @param task the code block to execute. - * @return a Future representing pending completion of the task - */ - @Override - public Future call(final ExecutorService executorService, final Callable task) { - return executorService.submit( - new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task)); - } - - /** - * Wraps the provided Runnable method with a Runnable method that will instantiate the Scoped and Thread - * Contexts in the target Thread before the caller's run method is called. - * @param task the Runnable task to perform. - * @return a Runnable. - */ - @Override - public Runnable wrap(Runnable task) { - return new Runner(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); - } - - /** - * Wraps the provided Callable method with a Callable method that will instantiate the Scoped and Thread - * Contexts in the target Thread before the caller's call method is called. - * @param task the Callable task to perform. - * @return a Callable. - */ - @Override - public Callable wrap(Callable task) { - return new Caller<>(this, ThreadContext.getContext(), ThreadContext.getImmutableStack(), task); - } - - private QueuedScopedContextProvider getProvider() { - return provider; - } - } - - 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( - final Instance context, - final Map threadContextMap, - final ThreadContext.ContextStack contextStack, - final Runnable op) { - this.context = context; - this.threadContextMap = threadContextMap; - this.contextStack = contextStack; - this.op = op; - } - - @Override - public void run() { - setupContext(contextMap, threadContextMap, contextStack, context); - try { - op.run(); - } finally { - context.getProvider().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( - final Instance context, - final Map threadContextMap, - final ThreadContext.ContextStack contextStack, - final Callable op) { - this.context = context; - this.threadContextMap = threadContextMap; - this.contextStack = contextStack; - this.op = op; - } - - @Override - public R call() throws Exception { - setupContext(contextMap, threadContextMap, contextStack, context); - try { - return op.call(); - } finally { - context.getProvider().removeScopedContext(); - ThreadContext.clearAll(); - } - } - } } From 3ef49c59c3fb439d490abfc7709e71624c022ba1 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Thu, 11 Apr 2024 10:32:21 -0700 Subject: [PATCH 19/19] Fix javadoc 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 e792d124a4c..6d72006ad91 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, since FormattedMessage behavior changede. + * Bumped to 2.24.0, since FormattedMessage behavior changed. */ @Version("2.24.0") package org.apache.logging.log4j.message;