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..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,13 +20,16 @@ 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; +import org.apache.logging.log4j.message.ParameterizedMapMessage; import org.apache.logging.log4j.spi.AbstractLogger; /** @@ -79,12 +82,21 @@ 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(" Map 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..c9a8ae691ed --- /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..17320a74af6 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/ResourceLogger.java @@ -0,0 +1,152 @@ +/* + * 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.MessageFactory; +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; + +/** + * 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 ExtendedLoggerWrapper { + private static final long serialVersionUID = -5837924138744974513L; + private final Supplier> supplier; + + 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, MessageFactory messageFactory) { + super(logger, logger.getName(), messageFactory); + this.supplier = supplier; + } + + @Override + public void logMessage(String fqcn, Level level, Marker marker, Message message, Throwable t) { + if (supplier != null) { + ScopedContext.runWhere(supplier.get(), () -> logger.logMessage(fqcn, level, marker, message, t)); + } else { + 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; + private MessageFactory messageFactory; + + /** + * 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; + } + + /** + * 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. + */ + 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, 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 new file mode 100644 index 00000000000..b105e3be1d1 --- /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()) { + renderableMap.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()) { + renderableMap.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..a66f4459ac6 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/ResourceLoggerTest.java @@ -0,0 +1,160 @@ +/* + * 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.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +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.apache.logging.log4j.util.ReadOnlyStringMap; +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)); + 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", events.get(0).getMessage().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)); + map = events.get(0).getContextData(); + assertNotNull(map); + data = map.toMap(); + 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)); + 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", 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 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/_release-notes/_2.x.x.adoc b/src/site/_release-notes/_2.x.x.adoc index 7c597ddeb0f..1aba83cb5db 100644 --- a/src/site/_release-notes/_2.x.x.adoc +++ b/src/site/_release-notes/_2.x.x.adoc @@ -34,6 +34,7 @@ This releases contains ... * Fix usage of `log4j-api` in GraalVM without additional reachability data. (https://github.com/apache/logging-log4j2/pull/1539[1539]) * Centralize initialization in the `Provider` class and deprecate `log4j2.loggerContextFactory` property. (https://github.com/apache/logging-log4j2/pull/2374[2374]) +* Remove `log4j-kubernetes` lookup. User should migrate to https://github.com/fabric8io/kubernetes-client/blob/main/doc/KubernetesLog4j.md[`kubernetes-log4j`] (https://github.com/apache/logging-log4j2/pull/2412[2412]) [#release-notes-2-x-x-fixed] === Fixed @@ -43,6 +44,7 @@ This releases contains ... [#release-notes-2-x-x-updated] === Updated +* Add ScopedContext to log4j-api and ScopedContextDataProvider in log4j-core. (https://github.com/apache/logging-log4j-kotlin/issues/71[kotlin-71], https://github.com/apache/logging-log4j2/discussions/2214[2214]) * Update `actions/checkout` to version `4.1.2` (https://github.com/apache/logging-log4j2/pull/2370[2370]) * Update `co.elastic.clients:elasticsearch-java` to version `8.13.0` (https://github.com/apache/logging-log4j2/pull/2413[2413]) * Update `com.fasterxml.jackson:jackson-bom` to version `2.17.0` (https://github.com/apache/logging-log4j2/pull/2372[2372]) diff --git a/src/site/asciidoc/docs.adoc b/src/site/asciidoc/docs.adoc index effdc6e128b..3bc8c0d0929 100644 --- a/src/site/asciidoc/docs.adoc +++ b/src/site/asciidoc/docs.adoc @@ -29,6 +29,8 @@ * xref:/manual/eventlogging.html[Event Logging] * xref:/manual/messages.html[Messages] * xref:/manual/thread-context.html[ThreadContext] +* xref:/manual/scoped-context.html[ScopedContext] +* xref:/manual/resource-logger.html[ResourceLogger] == Configuration diff --git a/src/site/asciidoc/manual/resource-logger.adoc b/src/site/asciidoc/manual/resource-logger.adoc new file mode 100644 index 00000000000..289b69a3443 --- /dev/null +++ b/src/site/asciidoc/manual/resource-logger.adoc @@ -0,0 +1,90 @@ +//// + 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 + +---- +%X %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 +---- + +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. diff --git a/src/site/asciidoc/manual/scoped-context.adoc b/src/site/asciidoc/manual/scoped-context.adoc new file mode 100644 index 00000000000..592945cf7e9 --- /dev/null +++ b/src/site/asciidoc/manual/scoped-context.adoc @@ -0,0 +1,118 @@ +//// + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +//// += Log4j 2 API +Ralph Goers ; + +== 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