diff --git a/log4j-api-test/pom.xml b/log4j-api-test/pom.xml
index e1fb806b9b4..aef8a1b5d64 100644
--- a/log4j-api-test/pom.xml
+++ b/log4j-api-test/pom.xml
@@ -144,6 +144,11 @@
mockito-inline
test
+
+ org.jspecify
+ jspecify
+ test
+
org.osgi
diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml
index f770ea88703..1c1f771c5d2 100644
--- a/log4j-core-test/pom.xml
+++ b/log4j-core-test/pom.xml
@@ -225,6 +225,11 @@
javax.jms-api
test
+
+ org.jspecify
+ jspecify
+ test
+
com.sun.mail
javax.mail
diff --git a/log4j-core-test/src/test/java/foo/TestFriendlyException.java b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
new file mode 100644
index 00000000000..4ce0c7d5a06
--- /dev/null
+++ b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
@@ -0,0 +1,155 @@
+/*
+ * 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 foo;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.util.Constants;
+
+/**
+ * A testing friendly exception featuring
+ *
+ * - Distinct localized message
+ * - Non-Log4j package origin1
+ * - Sufficient causal chain depth
+ * - Cyclic causal chain
+ * - Suppressed exceptions
+ * - Clutter-free stack trace (i.e., elements from JUnit, JDK, etc.)
+ * - Stack trace elements from named modules2
+ *
+ *
+ * 1 Helps with observing stack trace manipulation effects of Log4j.
+ *
+ *
+ * 2 Helps with testing module name serialization.
+ *
+ */
+public final class TestFriendlyException extends RuntimeException {
+
+ static {
+ // Ensure the distinct packaging
+ assertThat(TestFriendlyException.class.getPackage().getName()).doesNotStartWith("org.apache");
+ }
+
+ public static final StackTraceElement NAMED_MODULE_STACK_TRACE_ELEMENT = namedModuleStackTraceElement();
+
+ @SuppressWarnings("resource")
+ private static StackTraceElement namedModuleStackTraceElement() {
+ try {
+ new Socket("0.0.0.0", -1);
+ } catch (final Exception error) {
+ final StackTraceElement[] stackTraceElements = error.getStackTrace();
+ final String socketClassName = Socket.class.getCanonicalName();
+ for (final StackTraceElement stackTraceElement : stackTraceElements) {
+ if (stackTraceElement.getClassName().equals(socketClassName)) {
+ if (Constants.JAVA_MAJOR_VERSION > 8) {
+ final String stackTraceElementString = stackTraceElement.toString();
+ assertThat(stackTraceElementString).startsWith("java.base/");
+ }
+ return stackTraceElement;
+ }
+ }
+ }
+ throw new IllegalStateException("should not have reached here");
+ }
+
+ private static final String[] EXCLUDED_CLASS_NAME_PREFIXES = {
+ "java.lang", "jdk.internal", "org.junit", "sun.reflect"
+ };
+
+ public static final TestFriendlyException INSTANCE = create("r", 0, 2, new boolean[] {false}, new boolean[] {true});
+
+ private static TestFriendlyException create(
+ final String name,
+ final int depth,
+ final int maxDepth,
+ final boolean[] circular,
+ final boolean[] namedModuleAllowed) {
+ final TestFriendlyException error = new TestFriendlyException(name, namedModuleAllowed);
+ if (depth < maxDepth) {
+ final TestFriendlyException cause = create(name + "_c", depth + 1, maxDepth, circular, namedModuleAllowed);
+ error.initCause(cause);
+ final TestFriendlyException suppressed =
+ create(name + "_s", depth + 1, maxDepth, circular, namedModuleAllowed);
+ error.addSuppressed(suppressed);
+ final boolean circularAllowed = depth + 1 == maxDepth && !circular[0];
+ if (circularAllowed) {
+ cause.initCause(error);
+ suppressed.initCause(error);
+ circular[0] = true;
+ }
+ }
+ return error;
+ }
+
+ private TestFriendlyException(final String message, final boolean[] namedModuleAllowed) {
+ super(message);
+ removeExcludedStackTraceElements(namedModuleAllowed);
+ }
+
+ private void removeExcludedStackTraceElements(final boolean[] namedModuleAllowed) {
+ final StackTraceElement[] oldStackTrace = getStackTrace();
+ final boolean[] seenExcludedStackTraceElement = {false};
+ final StackTraceElement[] newStackTrace = Arrays.stream(oldStackTrace)
+ .flatMap(stackTraceElement ->
+ mapStackTraceElement(stackTraceElement, namedModuleAllowed, seenExcludedStackTraceElement))
+ .toArray(StackTraceElement[]::new);
+ setStackTrace(newStackTrace);
+ }
+
+ private static Stream mapStackTraceElement(
+ final StackTraceElement stackTraceElement,
+ final boolean[] namedModuleAllowed,
+ final boolean[] seenExcludedStackTraceElement) {
+ final Stream filteredStackTraceElement =
+ filterStackTraceElement(stackTraceElement, seenExcludedStackTraceElement);
+ final Stream javaBaseIncludedStackTraceElement =
+ namedModuleIncludedStackTraceElement(namedModuleAllowed);
+ return Stream.concat(javaBaseIncludedStackTraceElement, filteredStackTraceElement);
+ }
+
+ private static Stream filterStackTraceElement(
+ final StackTraceElement stackTraceElement, final boolean[] seenExcludedStackTraceElement) {
+ if (seenExcludedStackTraceElement[0]) {
+ return Stream.empty();
+ }
+ final String className = stackTraceElement.getClassName();
+ for (final String excludedClassNamePrefix : EXCLUDED_CLASS_NAME_PREFIXES) {
+ if (className.startsWith(excludedClassNamePrefix)) {
+ seenExcludedStackTraceElement[0] = true;
+ return Stream.empty();
+ }
+ }
+ return Stream.of(stackTraceElement);
+ }
+
+ private static Stream namedModuleIncludedStackTraceElement(final boolean[] namedModuleAllowed) {
+ if (!namedModuleAllowed[0]) {
+ return Stream.of();
+ }
+ namedModuleAllowed[0] = false;
+ return Stream.of(NAMED_MODULE_STACK_TRACE_ELEMENT);
+ }
+
+ @Override
+ public String getLocalizedMessage() {
+ return getMessage() + " [localized]";
+ }
+}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java
index 0688fcbb350..8ceaf0450b2 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/EventParameterMemoryLeakTest.java
@@ -18,6 +18,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -63,15 +64,20 @@ public void testParametersAreNotLeaked() throws Exception {
final String line1 = reader.readLine();
final String line2 = reader.readLine();
final String line3 = reader.readLine();
+ // line4 is empty line because of the line separator after throwable pattern
final String line4 = reader.readLine();
final String line5 = reader.readLine();
+ final String line6 = reader.readLine();
+ final String line7 = reader.readLine();
reader.close();
file.delete();
assertThat(line1, containsString("Message with parameter paramValue"));
assertThat(line2, containsString("paramValue"));
assertThat(line3, containsString("paramValue"));
- assertThat(line4, containsString("paramValue"));
- assertNull(line5, "Expected only three lines");
+ assertThat(line4, is(""));
+ assertThat(line5, containsString("paramValue"));
+ assertThat(line6, is(""));
+ assertNull(line7, "Expected only six lines");
final GarbageCollectionHelper gcHelper = new GarbageCollectionHelper();
gcHelper.run();
try {
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderOnStartupTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderOnStartupTest.java
index 9aa469aa7f2..3e419755c8c 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderOnStartupTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingAppenderOnStartupTest.java
@@ -51,7 +51,7 @@ public class RollingAppenderOnStartupTest {
@BeforeAll
public static void setup() throws Exception {
final Path target = loggingPath.resolve(FILENAME);
- Files.copy(Paths.get(SOURCE, FILENAME), target, StandardCopyOption.COPY_ATTRIBUTES);
+ Files.copy(Paths.get(SOURCE, FILENAME), target, StandardCopyOption.REPLACE_EXISTING);
final FileTime newTime = FileTime.from(Instant.now().minus(1, ChronoUnit.DAYS));
final BasicFileAttributeView attrs = Files.getFileAttributeView(target, BasicFileAttributeView.class);
attrs.setTimes(newTime, newTime, newTime);
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java
index 89158fd2ee7..42f8f58c21a 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/NestedLoggingFromThrowableMessageTest.java
@@ -80,7 +80,7 @@ public void testNestedLoggingInLastArgument() throws Exception {
final Set lines2 = readUniqueLines(file2);
assertEquals("Expected the same data from both appenders", lines1, lines2);
- assertEquals(2, lines1.size());
+ assertEquals(3, lines1.size());
assertTrue(lines1.contains("INFO NestedLoggingFromThrowableMessageTest Logging in getMessage "));
assertTrue(lines1.contains("ERROR NestedLoggingFromThrowableMessageTest Test message"));
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyHelperTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyHelperTest.java
deleted file mode 100644
index 0d95d83ff52..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyHelperTest.java
+++ /dev/null
@@ -1,79 +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.core.impl;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.Test;
-
-/**
- * Tests ThrowableProxyHelper.
- */
-public class ThrowableProxyHelperTest {
-
- /**
- * We populate dummy stack trace and array of stack trace elements in the right order
- * It supposed to always trigger fast path so cache won't be populated
- * This simulates the case when current thread's and throwable stack traces have the same elements
- */
- @Test
- public void testSuccessfulCacheHit() {
- final Map map = new HashMap<>();
- final Deque> stack = new ArrayDeque<>(3);
- final StackTraceElement[] stackTraceElements = new StackTraceElement[3];
- stackTraceElements[0] = new StackTraceElement(Integer.class.getName(), "toString", "Integer.java", 1);
- stack.addLast(Integer.class);
- stackTraceElements[1] = new StackTraceElement(Float.class.getName(), "toString", "Float.java", 1);
- stack.addLast(Float.class);
- stackTraceElements[2] = new StackTraceElement(Double.class.getName(), "toString", "Double.java", 1);
- stack.addLast(Double.class);
- final Throwable throwable = new IllegalStateException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
- ThrowableProxyHelper.toExtendedStackTrace(proxy, stack, map, null, stackTraceElements);
- assertTrue(map.isEmpty());
- }
-
- /**
- * We populate dummy stack trace and array of stack trace elements in the wrong order
- * It will trigger fast path only once so cache will have two items
- */
- @Test
- public void testFailedCacheHit() {
- final Map map = new HashMap<>();
- final Deque> stack = new ArrayDeque<>(3);
- final StackTraceElement[] stackTraceElements = new StackTraceElement[3];
- stackTraceElements[0] = new StackTraceElement(Integer.class.getName(), "toString", "Integer.java", 1);
- stack.addFirst(Integer.class);
- stackTraceElements[1] = new StackTraceElement(Float.class.getName(), "toString", "Float.java", 1);
- stack.addFirst(Float.class);
- stackTraceElements[2] = new StackTraceElement(Double.class.getName(), "toString", "Double.java", 1);
- stack.addFirst(Double.class);
- final Throwable throwable = new IllegalStateException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
- ThrowableProxyHelper.toExtendedStackTrace(proxy, stack, map, null, stackTraceElements);
- assertFalse(map.isEmpty());
- // Integer will match, so fast path won't cache it, only Float and Double will appear in cache after class
- // loading
- assertTrue(map.containsKey(Double.class.getName()));
- assertTrue(map.containsKey(Float.class.getName()));
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyTest.java
deleted file mode 100644
index 13246f19c61..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyTest.java
+++ /dev/null
@@ -1,462 +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.core.impl;
-
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.net.BindException;
-import java.net.InetSocketAddress;
-import java.nio.channels.ServerSocketChannel;
-import java.security.Permission;
-import java.security.SecureRandom;
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.Map;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.KeyGenerator;
-import javax.crypto.spec.IvParameterSpec;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.config.plugins.convert.Base64Converter;
-import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper;
-import org.apache.logging.log4j.core.jackson.Log4jXmlObjectMapper;
-import org.apache.logging.log4j.core.pattern.PlainTextRenderer;
-import org.apache.logging.log4j.util.Constants;
-import org.apache.logging.log4j.util.Strings;
-import org.junit.jupiter.api.Test;
-
-/**
- *
- */
-public class ThrowableProxyTest {
-
- public static class AlwaysThrowsError {
- static {
- if (true) {
- throw new Error("I always throw an Error when initialized");
- }
- }
- }
-
- static class Fixture {
- @JsonProperty
- ThrowableProxy proxy = new ThrowableProxy(new IOException("test"));
- }
-
- private ThrowableProxy deserialize(final byte[] binary) throws IOException, ClassNotFoundException {
- final ByteArrayInputStream inArr = new ByteArrayInputStream(binary);
- final ObjectInputStream in = new ObjectInputStream(inArr);
- return (ThrowableProxy) in.readObject();
- }
-
- private byte[] serialize(final ThrowableProxy proxy) throws IOException {
- final ByteArrayOutputStream arr = new ByteArrayOutputStream();
- final ObjectOutputStream out = new ObjectOutputStream(arr);
- out.writeObject(proxy);
- return arr.toByteArray();
- }
-
- private boolean allLinesContain(final String text, final String containedText) {
- final String[] lines = text.split("\n");
- for (final String line : lines) {
- if (line.isEmpty()) {
- continue;
- }
- if (!line.contains(containedText)) {
- return false;
- }
- }
- return true;
- }
-
- private boolean lastLineContains(final String text, final String containedText) {
- final String[] lines = text.split("\n");
- final String lastLine = lines[lines.length - 1];
- return lastLine.contains(containedText);
- }
-
- private void testIoContainer(final ObjectMapper objectMapper) throws IOException {
- final Fixture expected = new Fixture();
- final String s = objectMapper.writeValueAsString(expected);
- final Fixture actual = objectMapper.readValue(s, Fixture.class);
- assertEquals(expected.proxy.getName(), actual.proxy.getName());
- assertEquals(expected.proxy.getMessage(), actual.proxy.getMessage());
- assertEquals(expected.proxy.getLocalizedMessage(), actual.proxy.getLocalizedMessage());
- assertEquals(expected.proxy.getCommonElementCount(), actual.proxy.getCommonElementCount());
- assertArrayEquals(expected.proxy.getExtendedStackTrace(), actual.proxy.getExtendedStackTrace());
- assertEquals(expected.proxy, actual.proxy);
- }
-
- @Test
- public void testIoContainerAsJson() throws IOException {
- testIoContainer(new Log4jJsonObjectMapper());
- }
-
- @Test
- public void testIoContainerAsXml() throws IOException {
- testIoContainer(new Log4jXmlObjectMapper());
- }
-
- /**
- * Attempts to instantiate a class that cannot initialize and then logs the stack trace of the Error. The logger
- * must not fail when using {@link ThrowableProxy} to inspect the frames of the stack trace.
- */
- @Test
- public void testLogStackTraceWithClassThatCannotInitialize() {
- final Error e = assertThrows(Error.class, AlwaysThrowsError::new);
- // Print the stack trace to System.out for informational purposes
- // System.err.println("### Here's the stack trace that we'll log with log4j ###");
- // e.printStackTrace();
- // System.err.println("### End stack trace ###");
-
- final Logger logger = LogManager.getLogger(getClass());
-
- assertDoesNotThrow(() -> {
- // This is the critical portion of the test. The log message must be printed without
- // throwing a java.lang.Error when introspecting the AlwaysThrowError class in the
- // stack trace.
- logger.error(e.getMessage(), e);
- logger.error(e);
- });
- }
-
- @Test
- public void testLogStackTraceWithClassThatWillCauseSecurityException() throws IOException {
- final SecurityManager sm = System.getSecurityManager();
- try {
- System.setSecurityManager(new SecurityManager() {
- @Override
- public void checkPermission(final Permission perm) {
- if (perm instanceof RuntimePermission) {
- // deny access to the class to trigger the security exception
- if ("accessClassInPackage.sun.nio.ch".equals(perm.getName())) {
- throw new SecurityException(perm.toString());
- }
- }
- }
- });
- final BindException e = assertThrows(BindException.class, () -> {
- ServerSocketChannel.open().socket().bind(new InetSocketAddress("localhost", 9300));
- ServerSocketChannel.open().socket().bind(new InetSocketAddress("localhost", 9300));
- });
- assertDoesNotThrow(() -> new ThrowableProxy(e));
- } finally {
- // restore the security manager
- System.setSecurityManager(sm);
- }
- }
-
- @Test
- public void testLogStackTraceWithClassLoaderThatWithCauseSecurityException() throws Exception {
- final SecurityManager sm = System.getSecurityManager();
- try {
- System.setSecurityManager(new SecurityManager() {
- @Override
- public void checkPermission(final Permission perm) {
- if (perm instanceof RuntimePermission) {
- // deny access to the classloader to trigger the security exception
- if ("getClassLoader".equals(perm.getName())) {
- throw new SecurityException(perm.toString());
- }
- }
- }
- });
- final String algorithm = "AES/CBC/PKCS5Padding";
- final Cipher ec = Cipher.getInstance(algorithm);
- final byte[] bytes = new byte[16]; // initialization vector
- final SecureRandom secureRandom = new SecureRandom();
- secureRandom.nextBytes(bytes);
- final KeyGenerator generator = KeyGenerator.getInstance("AES");
- generator.init(128);
- final IvParameterSpec algorithmParameterSpec = new IvParameterSpec(bytes);
- ec.init(Cipher.ENCRYPT_MODE, generator.generateKey(), algorithmParameterSpec, secureRandom);
- final byte[] raw = Constants.EMPTY_BYTE_ARRAY;
- final byte[] encrypted = ec.doFinal(raw);
- final Cipher dc = Cipher.getInstance(algorithm);
- dc.init(Cipher.DECRYPT_MODE, generator.generateKey(), algorithmParameterSpec, secureRandom);
- final BadPaddingException e = assertThrows(BadPaddingException.class, () -> dc.doFinal(encrypted));
- assertDoesNotThrow(() -> new ThrowableProxy(e));
- } finally {
- // restore the existing security manager
- System.setSecurityManager(sm);
- }
- }
-
- // DO NOT REMOVE THIS COMMENT:
- // UNCOMMENT WHEN GENERATING SERIALIZED THROWABLEPROXY FOR #testSerializationWithUnknownThrowable
- // public static class DeletedException extends Exception {
- // private static final long serialVersionUID = 1L;
- //
- // public DeletedException(String msg) {
- // super(msg);
- // }
- // };
-
- @Test
- public void testSerialization() throws Exception {
- final Throwable throwable = new IllegalArgumentException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
- final byte[] binary = serialize(proxy);
- final ThrowableProxy proxy2 = deserialize(binary);
-
- assertEquals(proxy.getName(), proxy2.getName());
- assertEquals(proxy.getMessage(), proxy2.getMessage());
- assertEquals(proxy.getCauseProxy(), proxy2.getCauseProxy());
- assertArrayEquals(proxy.getExtendedStackTrace(), proxy2.getExtendedStackTrace());
- }
-
- @Test
- public void testSerialization_getExtendedStackTraceAsString() throws Exception {
- final Throwable throwable = new IllegalArgumentException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
- final byte[] binary = serialize(proxy);
- final ThrowableProxy proxy2 = deserialize(binary);
-
- assertEquals(
- proxy.getExtendedStackTraceAsString(Strings.EMPTY),
- proxy2.getExtendedStackTraceAsString(Strings.EMPTY));
- }
-
- @Test
- public void testSerialization_getExtendedStackTraceAsStringWithNestedThrowableDepth1() throws Exception {
- final Throwable throwable = new RuntimeException(new IllegalArgumentException("This is a test"));
- testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(throwable);
- }
-
- @Test
- public void testSerialization_getExtendedStackTraceAsStringWithNestedThrowableDepth2() throws Exception {
- final Throwable throwable =
- new RuntimeException(new IllegalArgumentException("This is a test", new IOException("level 2")));
- testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(throwable);
- }
-
- @Test
- public void testSerialization_getExtendedStackTraceAsStringWithNestedThrowableDepth3() throws Exception {
- final Throwable throwable = new RuntimeException(new IllegalArgumentException(
- "level 1", new IOException("level 2", new IllegalStateException("level 3"))));
- testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(throwable);
- }
-
- private void testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(final Throwable throwable)
- throws Exception {
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
- final byte[] binary = serialize(proxy);
- final ThrowableProxy proxy2 = deserialize(binary);
-
- assertEquals(
- proxy.getExtendedStackTraceAsString(Strings.EMPTY),
- proxy2.getExtendedStackTraceAsString(Strings.EMPTY));
- }
-
- @Test
- public void testSerializationWithUnknownThrowable() throws Exception {
-
- final String msg = "OMG I've been deleted!";
-
- // DO NOT DELETE THIS COMMENT:
- // UNCOMMENT TO RE-GENERATE SERIALIZED EVENT WHEN UPDATING THIS TEST.
- // final Exception thrown = new DeletedException(msg);
- // final ThrowableProxy proxy = new ThrowableProxy(thrown);
- // final byte[] binary = serialize(proxy);
- // String base64 = DatatypeConverter.printBase64Binary(binary);
- // System.out.println("final String base64 = \"" + base64.replaceAll("\r\n", "\\\\r\\\\n\" +\r\n\"") + "\";");
-
- final String base64 =
- "rO0ABXNyADFvcmcuYXBhY2hlLmxvZ2dpbmcubG9nNGouY29yZS5pbXBsLlRocm93YWJsZVByb3h52cww1Zp7rPoCAAdJABJjb21tb25FbGVtZW50Q291bnRMAApjYXVzZVByb3h5dAAzTG9yZy9hcGFjaGUvbG9nZ2luZy9sb2c0ai9jb3JlL2ltcGwvVGhyb3dhYmxlUHJveHk7WwASZXh0ZW5kZWRTdGFja1RyYWNldAA/W0xvcmcvYXBhY2hlL2xvZ2dpbmcvbG9nNGovY29yZS9pbXBsL0V4dGVuZGVkU3RhY2tUcmFjZUVsZW1lbnQ7TAAQbG9jYWxpemVkTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAB21lc3NhZ2VxAH4AA0wABG5hbWVxAH4AA1sAEXN1cHByZXNzZWRQcm94aWVzdAA0W0xvcmcvYXBhY2hlL2xvZ2dpbmcvbG9nNGovY29yZS9pbXBsL1Rocm93YWJsZVByb3h5O3hwAAAAAHB1cgA/W0xvcmcuYXBhY2hlLmxvZ2dpbmcubG9nNGouY29yZS5pbXBsLkV4dGVuZGVkU3RhY2tUcmFjZUVsZW1lbnQ7ys+II6XHz7wCAAB4cAAAABhzcgA8b3JnLmFwYWNoZS5sb2dnaW5nLmxvZzRqLmNvcmUuaW1wbC5FeHRlbmRlZFN0YWNrVHJhY2VFbGVtZW504d7Pusa2kAcCAAJMAA5leHRyYUNsYXNzSW5mb3QANkxvcmcvYXBhY2hlL2xvZ2dpbmcvbG9nNGovY29yZS9pbXBsL0V4dGVuZGVkQ2xhc3NJbmZvO0wAEXN0YWNrVHJhY2VFbGVtZW50dAAdTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDt4cHNyADRvcmcuYXBhY2hlLmxvZ2dpbmcubG9nNGouY29yZS5pbXBsLkV4dGVuZGVkQ2xhc3NJbmZvAAAAAAAAAAECAANaAAVleGFjdEwACGxvY2F0aW9ucQB+AANMAAd2ZXJzaW9ucQB+AAN4cAF0AA10ZXN0LWNsYXNzZXMvdAABP3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgADTAAIZmlsZU5hbWVxAH4AA0wACm1ldGhvZE5hbWVxAH4AA3hwAAAAaHQANW9yZy5hcGFjaGUubG9nZ2luZy5sb2c0ai5jb3JlLmltcGwuVGhyb3dhYmxlUHJveHlUZXN0dAAXVGhyb3dhYmxlUHJveHlUZXN0LmphdmF0ACV0ZXN0U2VyaWFsaXphdGlvbldpdGhVbmtub3duVGhyb3dhYmxlc3EAfgAIc3EAfgAMAHEAfgAPdAAIMS43LjBfNTVzcQB+ABD////+dAAkc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBscHQAB2ludm9rZTBzcQB+AAhzcQB+AAwAcQB+AA9xAH4AF3NxAH4AEP////9xAH4AGXB0AAZpbnZva2VzcQB+AAhzcQB+AAwAcQB+AA9xAH4AF3NxAH4AEP////90AChzdW4ucmVmbGVjdC5EZWxlZ2F0aW5nTWV0aG9kQWNjZXNzb3JJbXBscHEAfgAec3EAfgAIc3EAfgAMAHEAfgAPcQB+ABdzcQB+ABD/////dAAYamF2YS5sYW5nLnJlZmxlY3QuTWV0aG9kcHEAfgAec3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAAAvdAApb3JnLmp1bml0LnJ1bm5lcnMubW9kZWwuRnJhbWV3b3JrTWV0aG9kJDF0ABRGcmFtZXdvcmtNZXRob2QuamF2YXQAEXJ1blJlZmxlY3RpdmVDYWxsc3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAAAMdAAzb3JnLmp1bml0LmludGVybmFsLnJ1bm5lcnMubW9kZWwuUmVmbGVjdGl2ZUNhbGxhYmxldAAXUmVmbGVjdGl2ZUNhbGxhYmxlLmphdmF0AANydW5zcQB+AAhzcQB+AAwBdAAOanVuaXQtNC4xMS5qYXJxAH4AD3NxAH4AEAAAACx0ACdvcmcuanVuaXQucnVubmVycy5tb2RlbC5GcmFtZXdvcmtNZXRob2RxAH4ALHQAEWludm9rZUV4cGxvc2l2ZWx5c3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAAARdAAyb3JnLmp1bml0LmludGVybmFsLnJ1bm5lcnMuc3RhdGVtZW50cy5JbnZva2VNZXRob2R0ABFJbnZva2VNZXRob2QuamF2YXQACGV2YWx1YXRlc3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAAEPdAAeb3JnLmp1bml0LnJ1bm5lcnMuUGFyZW50UnVubmVydAARUGFyZW50UnVubmVyLmphdmF0AAdydW5MZWFmc3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAABGdAAob3JnLmp1bml0LnJ1bm5lcnMuQmxvY2tKVW5pdDRDbGFzc1J1bm5lcnQAG0Jsb2NrSlVuaXQ0Q2xhc3NSdW5uZXIuamF2YXQACHJ1bkNoaWxkc3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAAAycQB+AE1xAH4ATnEAfgBPc3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAADudAAgb3JnLmp1bml0LnJ1bm5lcnMuUGFyZW50UnVubmVyJDNxAH4AR3EAfgA0c3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAAA/dAAgb3JnLmp1bml0LnJ1bm5lcnMuUGFyZW50UnVubmVyJDFxAH4AR3QACHNjaGVkdWxlc3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAADscQB+AEZxAH4AR3QAC3J1bkNoaWxkcmVuc3EAfgAIc3EAfgAMAXQADmp1bml0LTQuMTEuamFycQB+AA9zcQB+ABAAAAA1cQB+AEZxAH4AR3QACmFjY2VzcyQwMDBzcQB+AAhzcQB+AAwBdAAOanVuaXQtNC4xMS5qYXJxAH4AD3NxAH4AEAAAAOV0ACBvcmcuanVuaXQucnVubmVycy5QYXJlbnRSdW5uZXIkMnEAfgBHcQB+AEFzcQB+AAhzcQB+AAwBdAAOanVuaXQtNC4xMS5qYXJxAH4AD3NxAH4AEAAAATVxAH4ARnEAfgBHcQB+ADRzcQB+AAhzcQB+AAwBdAAELmNwL3EAfgAPc3EAfgAQAAAAMnQAOm9yZy5lY2xpcHNlLmpkdC5pbnRlcm5hbC5qdW5pdDQucnVubmVyLkpVbml0NFRlc3RSZWZlcmVuY2V0ABhKVW5pdDRUZXN0UmVmZXJlbmNlLmphdmFxAH4ANHNxAH4ACHNxAH4ADAF0AAQuY3AvcQB+AA9zcQB+ABAAAAAmdAAzb3JnLmVjbGlwc2UuamR0LmludGVybmFsLmp1bml0LnJ1bm5lci5UZXN0RXhlY3V0aW9udAASVGVzdEV4ZWN1dGlvbi5qYXZhcQB+ADRzcQB+AAhzcQB+AAwBdAAELmNwL3EAfgAPc3EAfgAQAAAB03QANm9yZy5lY2xpcHNlLmpkdC5pbnRlcm5hbC5qdW5pdC5ydW5uZXIuUmVtb3RlVGVzdFJ1bm5lcnQAFVJlbW90ZVRlc3RSdW5uZXIuamF2YXQACHJ1blRlc3Rzc3EAfgAIc3EAfgAMAXQABC5jcC9xAH4AD3NxAH4AEAAAAqtxAH4AgnEAfgCDcQB+AIRzcQB+AAhzcQB+AAwBdAAELmNwL3EAfgAPc3EAfgAQAAABhnEAfgCCcQB+AINxAH4ANHNxAH4ACHNxAH4ADAF0AAQuY3AvcQB+AA9zcQB+ABAAAADFcQB+AIJxAH4Ag3QABG1haW50ABZPTUcgSSd2ZSBiZWVuIGRlbGV0ZWQhcQB+AJJ0AEZvcmcuYXBhY2hlLmxvZ2dpbmcubG9nNGouY29yZS5pbXBsLlRocm93YWJsZVByb3h5VGVzdCREZWxldGVkRXhjZXB0aW9udXIANFtMb3JnLmFwYWNoZS5sb2dnaW5nLmxvZzRqLmNvcmUuaW1wbC5UaHJvd2FibGVQcm94eTv67QHghaLrOQIAAHhwAAAAAA==";
-
- final byte[] binaryDecoded = Base64Converter.parseBase64Binary(base64);
- final ThrowableProxy proxy2 = deserialize(binaryDecoded);
-
- assertEquals(this.getClass().getName() + "$DeletedException", proxy2.getName());
- assertEquals(msg, proxy2.getMessage());
- }
-
- @Test
- public void testSeparator_getExtendedStackTraceAsString() throws Exception {
- final Throwable throwable = new IllegalArgumentException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
-
- final String separator = " | ";
- final String extendedStackTraceAsString =
- proxy.getExtendedStackTraceAsString(null, PlainTextRenderer.getInstance(), " | ", Strings.EMPTY);
- assertTrue(allLinesContain(extendedStackTraceAsString, separator), extendedStackTraceAsString);
- }
-
- @Test
- public void testSuffix_getExtendedStackTraceAsString() throws Exception {
- final Throwable throwable = new IllegalArgumentException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
-
- final String suffix = "some suffix";
- final String extendedStackTraceAsString = proxy.getExtendedStackTraceAsString(suffix);
- assertTrue(lastLineContains(extendedStackTraceAsString, suffix), extendedStackTraceAsString);
- }
-
- @Test
- public void testSuffix_getExtendedStackTraceAsStringWithCausedThrowable() throws Exception {
- final Throwable throwable = new RuntimeException(new IllegalArgumentException("This is a test"));
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
-
- final String suffix = "some suffix";
- assertTrue(allLinesContain(proxy.getExtendedStackTraceAsString(suffix), suffix));
- }
-
- @Test
- public void testSuffix_getExtendedStackTraceAsStringWithSuppressedThrowable() throws Exception {
- final IllegalArgumentException cause = new IllegalArgumentException("This is a test");
- final Throwable throwable = new RuntimeException(cause);
- throwable.addSuppressed(new IOException("This is a test"));
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
-
- final String suffix = "some suffix";
- assertTrue(allLinesContain(proxy.getExtendedStackTraceAsString(suffix), suffix));
- }
-
- @Test
- public void testSuffix_getCauseStackTraceAsString() throws Exception {
- final Throwable throwable = new IllegalArgumentException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
-
- final String suffix = "some suffix";
- assertTrue(allLinesContain(proxy.getCauseStackTraceAsString(suffix), suffix));
- }
-
- @Test
- public void testSuffix_getCauseStackTraceAsStringWithCausedThrowable() throws Exception {
- final Throwable throwable = new RuntimeException(new IllegalArgumentException("This is a test"));
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
-
- final String suffix = "some suffix";
- assertTrue(allLinesContain(proxy.getCauseStackTraceAsString(suffix), suffix));
- }
-
- @Test
- public void testSuffix_getCauseStackTraceAsStringWithSuppressedThrowable() throws Exception {
- final IllegalArgumentException cause = new IllegalArgumentException("This is a test");
- final Throwable throwable = new RuntimeException(cause);
- throwable.addSuppressed(new IOException("This is a test"));
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
-
- final String suffix = "some suffix";
- assertTrue(allLinesContain(proxy.getCauseStackTraceAsString(suffix), suffix));
- }
-
- @Test
- public void testStack() {
- final Map map = new HashMap<>();
- final Deque> stack = new ArrayDeque<>();
- final Throwable throwable = new IllegalStateException("This is a test");
- final ThrowableProxy proxy = new ThrowableProxy(throwable);
- final ExtendedStackTraceElement[] callerPackageData =
- ThrowableProxyHelper.toExtendedStackTrace(proxy, stack, map, null, throwable.getStackTrace());
- assertNotNull(callerPackageData, "No package data returned");
- }
-
- /**
- * Asserts that LOG4J2-834 is solved by constructing a ThrowableProxy over a RuntimeException object thrown at a
- * unloaded known class (already compiled and available as a test resource:
- * org.apache.logging.log4j.core.impl.ForceNoDefClassFoundError.class).
- */
- @Test
- public void testStackWithUnloadableClass() throws Exception {
- final Deque> stack = new ArrayDeque<>();
- final Map map = new HashMap<>();
-
- final String runtimeExceptionThrownAtUnloadableClass_base64 =
- "rO0ABXNyABpqYXZhLmxhbmcuUnVudGltZUV4Y2VwdGlvbp5fBkcKNIPlAgAAeHIAE2phdmEubGFuZy5FeGNlcHRpb27Q/R8+GjscxAIAAHhyABNqYXZhLmxhbmcuVGhyb3dhYmxl1cY1Jzl3uMsDAANMAAVjYXVzZXQAFUxqYXZhL2xhbmcvVGhyb3dhYmxlO0wADWRldGFpbE1lc3NhZ2V0ABJMamF2YS9sYW5nL1N0cmluZztbAApzdGFja1RyYWNldAAeW0xqYXZhL2xhbmcvU3RhY2tUcmFjZUVsZW1lbnQ7eHBxAH4ABnB1cgAeW0xqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnQ7AkYqPDz9IjkCAAB4cAAAAAFzcgAbamF2YS5sYW5nLlN0YWNrVHJhY2VFbGVtZW50YQnFmiY23YUCAARJAApsaW5lTnVtYmVyTAAOZGVjbGFyaW5nQ2xhc3NxAH4ABEwACGZpbGVOYW1lcQB+AARMAAptZXRob2ROYW1lcQB+AAR4cAAAAAZ0ADxvcmcuYXBhY2hlLmxvZ2dpbmcubG9nNGouY29yZS5pbXBsLkZvcmNlTm9EZWZDbGFzc0ZvdW5kRXJyb3J0AB5Gb3JjZU5vRGVmQ2xhc3NGb3VuZEVycm9yLmphdmF0AARtYWlueA==";
- final byte[] binaryDecoded = Base64Converter.parseBase64Binary(runtimeExceptionThrownAtUnloadableClass_base64);
- final ByteArrayInputStream inArr = new ByteArrayInputStream(binaryDecoded);
- final ObjectInputStream in = new ObjectInputStream(inArr);
- final Throwable throwable = (Throwable) in.readObject();
- final ThrowableProxy subject = new ThrowableProxy(throwable);
-
- ThrowableProxyHelper.toExtendedStackTrace(subject, stack, map, null, throwable.getStackTrace());
- }
-
- /**
- * Tests LOG4J2-934.
- */
- @Test
- public void testCircularSuppressedExceptions() {
- final Exception e1 = new Exception();
- final Exception e2 = new Exception();
- e2.addSuppressed(e1);
- e1.addSuppressed(e2);
- LogManager.getLogger().error("Error", e1);
- }
-
- @Test
- public void testSuppressedExceptions() {
- final Exception e = new Exception("Root exception");
- e.addSuppressed(new IOException("Suppressed #1"));
- e.addSuppressed(new IOException("Suppressed #2"));
- LogManager.getLogger().error("Error", e);
- final ThrowableProxy proxy = new ThrowableProxy(e);
- final String extendedStackTraceAsString = proxy.getExtendedStackTraceAsString("same suffix");
- assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
- assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
- }
-
- @Test
- public void testCauseSuppressedExceptions() {
- final Exception cause = new Exception("Nested exception");
- cause.addSuppressed(new IOException("Suppressed #1"));
- cause.addSuppressed(new IOException("Suppressed #2"));
- LogManager.getLogger().error("Error", new Exception(cause));
- final ThrowableProxy proxy = new ThrowableProxy(new Exception("Root exception", cause));
- final String extendedStackTraceAsString = proxy.getExtendedStackTraceAsString("same suffix");
- assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
- assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
- }
-
- /**
- * Tests LOG4J2-934.
- */
- @Test
- public void testCircularSuppressedNestedException() {
- final Exception e1 = new Exception();
- final Exception e2 = new Exception(e1);
- e2.addSuppressed(e1);
- e1.addSuppressed(e2);
- LogManager.getLogger().error("Error", e1);
- }
-
- /**
- * .
- */
- @Test
- public void testCircularCauseExceptions() {
- final Exception e1 = new Exception();
- final Exception e2 = new Exception(e1);
- e1.initCause(e2);
- LogManager.getLogger().error("Error", e1);
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
index efeead57e50..b1418c62e44 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverterTest.java
@@ -16,166 +16,163 @@
*/
package org.apache.logging.log4j.core.pattern;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static java.util.Arrays.asList;
-import java.io.PrintWriter;
-import java.io.StringWriter;
+import foo.TestFriendlyException;
import java.util.List;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.impl.Log4jLogEvent;
-import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
-import org.apache.logging.log4j.core.impl.ThrowableProxy;
-import org.apache.logging.log4j.message.SimpleMessage;
-import org.apache.logging.log4j.util.Strings;
-import org.junit.jupiter.api.Test;
+import org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.AbstractPropertyTest;
+import org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.AbstractStackTraceTest;
+import org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.DepthTestCase;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
-public class ExtendedThrowablePatternConverterTest {
+/**
+ * {@link ExtendedThrowablePatternConverter} tests.
+ */
+class ExtendedThrowablePatternConverterTest {
- @Test
- public void testSuffixFromNormalPattern() {
- final String suffix = "suffix(%mdc{key})";
- ThreadContext.put("key", "test suffix ");
- final String[] options = {suffix};
- final ExtendedThrowablePatternConverter converter =
- ExtendedThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(result.contains("test suffix"), "No suffix");
- }
+ @Nested
+ class PropertyTest extends AbstractPropertyTest {
- @Test
- public void testSuffix() {
- final String suffix = "suffix(test suffix)";
- final String[] options = {suffix};
- final ExtendedThrowablePatternConverter converter =
- ExtendedThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(result.contains("test suffix"), "No suffix");
+ PropertyTest() {
+ super("%xEx");
+ }
}
- @Test
- public void testSuffixWillIgnoreThrowablePattern() {
- final String suffix = "suffix(%xEx{suffix(inner suffix)})";
- final String[] options = {suffix};
- final ExtendedThrowablePatternConverter converter =
- ExtendedThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertFalse(result.contains("inner suffix"), "Has unexpected suffix");
- }
+ private static final List EXPECTED_FULL_STACK_TRACE_LINES = asList(
+ "foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT + " ~[?:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.(ThrowablePatternConverterTest.java:0) [test-classes/:?]",
+ " Suppressed: foo.TestFriendlyException: r_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ " Caused by: foo.TestFriendlyException: r_s_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ "Caused by: foo.TestFriendlyException: r_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_c_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ " Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Caused by: foo.TestFriendlyException: r_c_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ "Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]");
- @Test
- public void testDeserializedLogEventWithThrowableProxyButNoThrowable() {
- final ExtendedThrowablePatternConverter converter = ExtendedThrowablePatternConverter.newInstance(null, null);
- final Throwable originalThrowable = new Exception("something bad happened");
- final ThrowableProxy throwableProxy = new ThrowableProxy(originalThrowable);
- final Throwable deserializedThrowable = null;
- final Log4jLogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("")) //
- .setThrown(deserializedThrowable) //
- .setThrownProxy(throwableProxy) //
- .setTimeMillis(0)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(result.contains(originalThrowable.getMessage()), result);
- assertTrue(result.contains(originalThrowable.getStackTrace()[0].getMethodName()), result);
- }
+ @Nested
+ class StackTraceTest extends AbstractStackTraceTest {
- @Test
- public void testFiltering() {
- final String packages = "filters(org.junit, org.apache.maven, sun.reflect, java.lang.reflect)";
- final String[] options = {packages};
- final ExtendedThrowablePatternConverter converter =
- ExtendedThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(result.contains(" suppressed "), "No suppressed lines");
- }
+ StackTraceTest() {
+ super("%xEx");
+ }
- @Test
- public void testFull() {
- final ExtendedThrowablePatternConverter converter = ExtendedThrowablePatternConverter.newInstance(null, null);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final StringWriter sw = new StringWriter();
- final PrintWriter pw = new PrintWriter(sw);
- parent.printStackTrace(pw);
- String result = sb.toString();
- result = result.replaceAll(" ~?\\[.*\\]", Strings.EMPTY);
- final String expected = sw.toString(); // .replaceAll("\r", Strings.EMPTY);
- assertEquals(expected, result);
- }
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#fullStackTracePatterns")
+ void full_output_should_match(final String pattern) {
+ final String effectivePattern = patternPrefix + pattern;
+ assertStackTraceLines(null, effectivePattern, EXPECTED_FULL_STACK_TRACE_LINES);
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_limited_output_should_match(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(depthTestCase, pattern, EXPECTED_FULL_STACK_TRACE_LINES);
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_and_package_limited_output_should_match_1(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}{filters(foo)}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(
+ depthTestCase,
+ pattern,
+ asList(
+ "foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT + " ~[?:?]",
+ " ... suppressed 2 lines",
+ " at org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.(ThrowablePatternConverterTest.java:0) [test-classes/:?]",
+ " Suppressed: foo.TestFriendlyException: r_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ " Caused by: foo.TestFriendlyException: r_s_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ "Caused by: foo.TestFriendlyException: r_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_c_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ " Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Caused by: foo.TestFriendlyException: r_c_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ "Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]"));
+ }
- @Test
- public void testFiltersAndSeparator() {
- final ExtendedThrowablePatternConverter exConverter = ExtendedThrowablePatternConverter.newInstance(
- null, new String[] {"full", "filters(org.junit,org.eclipse)", "separator(|)"});
- final ThrowableFormatOptions options = exConverter.getOptions();
- final List ignorePackages = options.getIgnorePackages();
- assertNotNull(ignorePackages);
- final String ignorePackagesString = ignorePackages.toString();
- assertTrue(ignorePackages.contains("org.junit"), ignorePackagesString);
- assertTrue(ignorePackages.contains("org.eclipse"), ignorePackagesString);
- assertEquals("|", options.getSeparator());
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_and_package_limited_output_should_match_2(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}{filters(org.apache)}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(
+ depthTestCase,
+ pattern,
+ asList(
+ "foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT + " ~[?:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ...",
+ " Suppressed: foo.TestFriendlyException: r_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ " Caused by: foo.TestFriendlyException: r_s_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ "Caused by: foo.TestFriendlyException: r_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_c_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ " Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Caused by: foo.TestFriendlyException: r_c_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+ " ... 3 more",
+ "Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]"));
+ }
}
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowableTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowableTest.java
deleted file mode 100644
index 639837c8042..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ExtendedThrowableTest.java
+++ /dev/null
@@ -1,52 +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.core.pattern;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.List;
-import org.apache.logging.log4j.Logger;
-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.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-@LoggerContextSource("log4j-throwablefilter.xml")
-public class ExtendedThrowableTest {
- private ListAppender app;
-
- @BeforeEach
- public void setUp(@Named("List") final ListAppender app) throws Exception {
- this.app = app.clear();
- }
-
- @Test
- public void testException(final LoggerContext context) {
- final Logger logger = context.getLogger("LoggerTest");
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- logger.error("Exception", parent);
- final List msgs = app.getMessages();
- assertNotNull(msgs);
- assertEquals(1, msgs.size(), "Incorrect number of messages. Should be 1 is " + msgs.size());
- assertTrue(msgs.get(0).contains("suppressed"), "No suppressed lines");
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java
index e069da72a01..b02618aff4b 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverterTest.java
@@ -16,132 +16,163 @@
*/
package org.apache.logging.log4j.core.pattern;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static java.util.Arrays.asList;
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.impl.Log4jLogEvent;
-import org.apache.logging.log4j.message.SimpleMessage;
-import org.junit.jupiter.api.Test;
+import foo.TestFriendlyException;
+import java.util.List;
+import org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.AbstractPropertyTest;
+import org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.AbstractStackTraceTest;
+import org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.DepthTestCase;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
-public class RootThrowablePatternConverterTest {
+/**
+ * {@link RootThrowablePatternConverter} tests.
+ */
+class RootThrowablePatternConverterTest {
- @Test
- public void testSuffix() {
- final String suffix = "suffix(test suffix)";
- final String[] options = {suffix};
- final RootThrowablePatternConverter converter = RootThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(result.contains("test suffix"), "No suffix");
- }
+ @Nested
+ class PropertyTest extends AbstractPropertyTest {
- @Test
- public void testSuffixFromNormalPattern() {
- final String suffix = "suffix(%mdc{key})";
- ThreadContext.put("key", "test suffix ");
- final String[] options = {suffix};
- final RootThrowablePatternConverter converter = RootThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(result.contains("test suffix"), "No suffix");
+ PropertyTest() {
+ super("%rEx");
+ }
}
- @Test
- public void testSuffixWillIgnoreThrowablePattern() {
- final String suffix = "suffix(%xEx{suffix(inner suffix)})";
- final String[] options = {suffix};
- final RootThrowablePatternConverter converter = RootThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertFalse(result.contains("inner suffix"), "Has unexpected suffix");
- }
+ private static final List EXPECTED_FULL_STACK_TRACE_LINES = asList(
+ "[CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Wrapped by: foo.TestFriendlyException: r_c_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Wrapped by: foo.TestFriendlyException: r_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ " Wrapped by: foo.TestFriendlyException: r_c_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Wrapped by: foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.(TestFriendlyException.java:0)",
+ " at org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.(ThrowablePatternConverterTest.java:0)",
+ " Suppressed: foo.TestFriendlyException: r_s_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ " Wrapped by: foo.TestFriendlyException: r_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more");
- @Test
- public void testFull1() {
- final RootThrowablePatternConverter converter = RootThrowablePatternConverter.newInstance(null, null);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- // System.out.print(result);
- assertTrue(
- result.contains("Wrapped by: java.lang.IllegalArgumentException: IllegalArgument"),
- "Missing Exception");
- assertTrue(result.startsWith("java.lang.NullPointerException: null pointer"), "Incorrect start of msg");
- }
+ @Nested
+ class StackTraceTest extends AbstractStackTraceTest {
+
+ StackTraceTest() {
+ super("%rEx");
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#fullStackTracePatterns")
+ void full_output_should_match(final String pattern) {
+ final String effectivePattern = patternPrefix + pattern;
+ assertStackTraceLines(null, effectivePattern, EXPECTED_FULL_STACK_TRACE_LINES);
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_limited_output_should_match(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(depthTestCase, pattern, EXPECTED_FULL_STACK_TRACE_LINES);
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_and_package_limited_output_should_match_1(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}{filters(foo)}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(
+ depthTestCase,
+ pattern,
+ asList(
+ "[CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Wrapped by: foo.TestFriendlyException: r_c_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ "Wrapped by: foo.TestFriendlyException: r_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 2 more",
+ " Suppressed: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ " Wrapped by: foo.TestFriendlyException: r_c_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ "Wrapped by: foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
+ " ... suppressed 2 lines",
+ " at org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.(ThrowablePatternConverterTest.java:0)",
+ " Suppressed: foo.TestFriendlyException: r_s_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ " Wrapped by: foo.TestFriendlyException: r_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more"));
+ }
- /**
- * Sanity check for testFull1() above, makes sure that the way testFull1 is written matches actually throwing
- * exceptions.
- */
- @Test
- public void testFull2() {
- final RootThrowablePatternConverter converter = RootThrowablePatternConverter.newInstance(null, null);
- Throwable parent;
- try {
- try {
- throw new NullPointerException("null pointer");
- } catch (final NullPointerException e) {
- throw new IllegalArgumentException("IllegalArgument", e);
- }
- } catch (final IllegalArgumentException e) {
- parent = e;
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_and_package_limited_output_should_match_2(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}{filters(org.apache)}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(
+ depthTestCase,
+ pattern,
+ asList(
+ "[CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Wrapped by: foo.TestFriendlyException: r_c_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Wrapped by: foo.TestFriendlyException: r_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ " Wrapped by: foo.TestFriendlyException: r_c_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Wrapped by: foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.(TestFriendlyException.java:0)",
+ " ...",
+ " Suppressed: foo.TestFriendlyException: r_s_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ " Wrapped by: foo.TestFriendlyException: r_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more"));
}
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- // System.out.print(result);
- assertTrue(
- result.contains("Wrapped by: java.lang.IllegalArgumentException: IllegalArgument"),
- "Missing Exception");
- assertTrue(result.startsWith("java.lang.NullPointerException: null pointer"), "Incorrect start of msg");
}
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowableTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowableTest.java
deleted file mode 100644
index 1bbd44aa5e6..00000000000
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/RootThrowableTest.java
+++ /dev/null
@@ -1,46 +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.core.pattern;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.List;
-import org.apache.logging.log4j.Logger;
-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;
-
-@LoggerContextSource("log4j-rootthrowablefilter.xml")
-public class RootThrowableTest {
- @Test
- public void testException(final LoggerContext context, @Named("List") final ListAppender app) {
- app.clear();
- final Logger logger = context.getLogger("LoggerTest");
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- logger.error("Exception", parent);
- final List msgs = app.getMessages();
- assertNotNull(msgs);
- assertEquals(1, msgs.size(), "Incorrect number of messages. Should be 1 is " + msgs.size());
- assertTrue(msgs.get(0).contains("suppressed"), "No suppressed lines");
- app.clear();
- }
-}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
index aa0e574ac8f..c886def43ce 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverterTest.java
@@ -16,241 +16,424 @@
*/
package org.apache.logging.log4j.core.pattern;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import foo.TestFriendlyException;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
-import org.apache.logging.log4j.core.util.Integers;
-import org.apache.logging.log4j.message.SimpleMessage;
-import org.apache.logging.log4j.util.Strings;
-import org.junit.jupiter.api.Test;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+/**
+ * {@link ThrowablePatternConverter} tests.
+ */
public class ThrowablePatternConverterTest {
- private static final class LocalizedException extends Exception {
+ private static final String NEWLINE = System.lineSeparator();
- private static final long serialVersionUID = 1L;
+ private static final Throwable EXCEPTION = TestFriendlyException.INSTANCE;
- @Override
- public String getLocalizedMessage() {
- return "I am localized.";
+ private static final StackTraceElement THROWING_METHOD = EXCEPTION.getStackTrace()[0];
+
+ private static final PatternParser PATTERN_PARSER = PatternLayout.createPatternParser(null);
+
+ private static final Level LEVEL = Level.FATAL;
+
+ static final class SeparatorTestCase {
+
+ final String patternAddendum;
+
+ private final String conversionEnding;
+
+ private SeparatorTestCase(final String patternAddendum, final String conversionEnding) {
+ this.patternAddendum = patternAddendum;
+ this.conversionEnding = conversionEnding;
}
- }
- private boolean everyLineEndsWith(final String text, final String suffix) {
- final String[] lines = text.split(Strings.LINE_SEPARATOR);
- for (final String line : lines) {
- if (!line.trim().endsWith(suffix)) {
- return false;
- }
+ @Override
+ public String toString() {
+ return String.format("{patternAddendum=`%s`, conversionEnding=`%s`}", patternAddendum, conversionEnding);
}
- return true;
}
- /**
- * TODO: Needs better a better exception? NumberFormatException is NOT helpful.
- */
- @Test
- public void testBadShortOption() {
- final String[] options = {"short.UNKNOWN"};
- assertThrows(NumberFormatException.class, () -> ThrowablePatternConverter.newInstance(null, options));
+ static Stream separatorTestCases() {
+ final String level = LEVEL.toString();
+ return Stream.of(
+ // Only separators
+ new SeparatorTestCase("{separator()}", ""),
+ new SeparatorTestCase("{separator(#)}", "#"),
+ // Only suffixes
+ new SeparatorTestCase("{suffix()}", NEWLINE),
+ new SeparatorTestCase("{suffix(~)}", " ~" + NEWLINE),
+ new SeparatorTestCase("{suffix(%level)}", " " + level + NEWLINE),
+ new SeparatorTestCase("{suffix(%rEx)}", NEWLINE),
+ // Both separators and suffixes
+ new SeparatorTestCase("{separator()}{suffix()}", ""),
+ new SeparatorTestCase("{separator()}{suffix(~)}", " ~"),
+ new SeparatorTestCase("{separator()}{suffix(%level)}", " " + level),
+ new SeparatorTestCase("{separator()}{suffix(%rEx)}", ""),
+ new SeparatorTestCase("{separator(#)}{suffix()}", "#"),
+ new SeparatorTestCase("{separator(#)}{suffix(~)}", " ~#"),
+ new SeparatorTestCase("{separator(#)}{suffix(%level)}", " " + level + "#"),
+ new SeparatorTestCase("{separator(#)}{suffix(%rEx)}", "#"));
}
- @Test
- public void testFull() {
- final String[] options = {"full"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- Throwable parent;
- try {
- try {
- throw new NullPointerException("null pointer");
- } catch (final NullPointerException e) {
- throw new IllegalArgumentException("IllegalArgument", e);
- }
- } catch (final IllegalArgumentException e) {
- parent = e;
+ @Nested
+ class PropertyTest extends AbstractPropertyTest {
+
+ PropertyTest() {
+ super("%ex");
}
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- // System.out.print(result);
- assertTrue(result.startsWith("java.lang.IllegalArgumentException: IllegalArgument"), "Incorrect start of msg");
- assertTrue(result.contains("java.lang.NullPointerException: null pointer"), "Missing nested exception");
}
- @Test
- public void testShortClassName() {
- final String packageName = "org.apache.logging.log4j.core.pattern.";
- final String[] options = {"short.className"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertEquals(packageName + "ThrowablePatternConverterTest", result, "The class names should be same");
+ abstract static class AbstractPropertyTest {
+
+ private final String patternPrefix;
+
+ AbstractPropertyTest(final String patternPrefix) {
+ this.patternPrefix = patternPrefix;
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
+ void message_should_be_rendered(final SeparatorTestCase separatorTestCase) {
+ assertConversion(separatorTestCase, "{short.message}", EXCEPTION.getMessage());
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
+ void localizedMessage_should_be_rendered(final SeparatorTestCase separatorTestCase) {
+ assertConversion(separatorTestCase, "{short.localizedMessage}", EXCEPTION.getLocalizedMessage());
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
+ void className_should_be_rendered(final SeparatorTestCase separatorTestCase) {
+ assertConversion(separatorTestCase, "{short.className}", THROWING_METHOD.getClassName());
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
+ void methodName_should_be_rendered(final SeparatorTestCase separatorTestCase) {
+ assertConversion(separatorTestCase, "{short.methodName}", THROWING_METHOD.getMethodName());
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
+ void lineNumber_should_be_rendered(final SeparatorTestCase separatorTestCase) {
+ assertConversion(separatorTestCase, "{short.lineNumber}", THROWING_METHOD.getLineNumber() + "");
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
+ void fileName_should_be_rendered(final SeparatorTestCase separatorTestCase) {
+ assertConversion(separatorTestCase, "{short.fileName}", THROWING_METHOD.getFileName());
+ }
+
+ private void assertConversion(
+ final SeparatorTestCase separatorTestCase, final String pattern, final Object expectedOutput) {
+ final String effectivePattern = patternPrefix + pattern + separatorTestCase.patternAddendum;
+ final String output = convert(effectivePattern);
+ final String effectiveExpectedOutput = expectedOutput + separatorTestCase.conversionEnding;
+ assertThat(output)
+ .as(
+ "pattern=`%s`, separatorTestCase=%s, expectedOutput=`%s`",
+ pattern, separatorTestCase, expectedOutput)
+ .isEqualTo(effectiveExpectedOutput);
+ }
}
- @Test
- public void testShortFileName() {
- final String[] options = {"short.fileName"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertEquals("ThrowablePatternConverterTest.java", result, "The file names should be same");
+ static final class DepthTestCase {
+
+ final SeparatorTestCase separatorTestCase;
+
+ final int maxLineCount;
+
+ private DepthTestCase(final SeparatorTestCase separatorTestCase, final int maxLineCount) {
+ this.separatorTestCase = separatorTestCase;
+ this.maxLineCount = maxLineCount;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{separatorTestCase=%s, maxLineCount=%d}", separatorTestCase, maxLineCount);
+ }
}
- @Test
- public void testShortLineNumber() {
- final String[] options = {"short.lineNumber"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final StackTraceElement top = parent.getStackTrace()[0];
- final int expectedLineNumber = top.getLineNumber();
-
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertEquals(Integers.parseInt(result), expectedLineNumber, "The line numbers should be same");
+ static Stream depthTestCases() {
+ return separatorTestCases().flatMap(separatorTestCase -> maxLineCounts()
+ .map(maxLineCount -> new DepthTestCase(separatorTestCase, maxLineCount)));
}
- @Test
- public void testShortLocalizedMessage() {
- final String[] options = {"short.localizedMessage"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- final Throwable parent = new LocalizedException();
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertEquals("I am localized.", result, "The messages should be same");
+ static Stream maxLineCounts() {
+ return Stream.of(0, 1, 2, 3, 4, 5, 10, 15, 20, Integer.MAX_VALUE);
}
- @Test
- public void testShortMessage() {
- final String[] options = {"short.message"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertEquals("IllegalArgument", result, "The messages should be same");
+ static Stream fullStackTracePatterns() {
+ return Stream.of("", "{}", "{full}", "{" + Integer.MAX_VALUE + "}", "{separator(" + NEWLINE + ")}");
}
- @Test
- public void testShortMethodName() {
- final String[] options = {"short.methodName"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertEquals("testShortMethodName", result, "The method names should be same");
+ @Nested
+ class StackTraceTest extends AbstractStackTraceTest {
+
+ StackTraceTest() {
+ super("%ex");
+ }
+
+ // This test does not provide `separator` and `suffix` options, since the reference output will be obtained from
+ // `Throwable#printStackTrace()`, which doesn't take these into account.
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#fullStackTracePatterns")
+ void full_output_should_match_Throwable_printStackTrace(final String pattern) {
+ final String expectedStackTrace = renderStackTraceUsingJava();
+ final String effectivePattern = patternPrefix + pattern;
+ final String actualStackTrace = convert(effectivePattern);
+ assertThat(actualStackTrace).as("pattern=`%s`", effectivePattern).isEqualTo(expectedStackTrace);
+ }
+
+ // This test does not provide `separator` and `suffix` options, since the reference output will be obtained from
+ // `Throwable#printStackTrace()`, which doesn't take these into account.
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#maxLineCounts")
+ void depth_limited_output_should_match_Throwable_printStackTrace(final int maxLineCount) {
+ final String expectedStackTrace = renderStackTraceUsingJava(maxLineCount);
+ final String effectivePattern = patternPrefix + '{' + maxLineCount + '}';
+ final String actualStackTrace = convert(effectivePattern);
+ assertThat(actualStackTrace).as("pattern=`%s`", effectivePattern).isEqualTo(expectedStackTrace);
+ }
+
+ private String renderStackTraceUsingJava(final int maxLineCount) {
+ if (maxLineCount == 0) {
+ return "";
+ }
+ final String stackTrace = renderStackTraceUsingJava();
+ if (maxLineCount == Integer.MAX_VALUE) {
+ return stackTrace;
+ }
+ return limitLines(stackTrace, maxLineCount);
+ }
+
+ private String limitLines(final String text, final int maxLineCount) {
+ final StringBuilder buffer = new StringBuilder();
+ int lineCount = 0;
+ int startIndex = 0;
+ int newlineIndex;
+ while (lineCount < maxLineCount && (newlineIndex = text.indexOf(NEWLINE, startIndex)) != -1) {
+ final int endIndex = newlineIndex + NEWLINE.length();
+ final String line = text.substring(startIndex, endIndex);
+ buffer.append(line);
+ lineCount++;
+ startIndex = endIndex;
+ }
+ return buffer.toString();
+ }
+
+ private String renderStackTraceUsingJava() {
+ final Charset charset = StandardCharsets.UTF_8;
+ try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final PrintStream printStream = new PrintStream(outputStream, false, charset.name())) {
+ EXCEPTION.printStackTrace(printStream);
+ printStream.flush();
+ return new String(outputStream.toByteArray(), charset);
+ } catch (final Exception error) {
+ throw new RuntimeException(error);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_limited_output_should_match(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(
+ depthTestCase,
+ pattern,
+ asList(
+ "foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.(TestFriendlyException.java:0)",
+ " at org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.(ThrowablePatternConverterTest.java:0)",
+ " Suppressed: foo.TestFriendlyException: r_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ " Caused by: foo.TestFriendlyException: r_s_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Caused by: foo.TestFriendlyException: r_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_c_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ " Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Caused by: foo.TestFriendlyException: r_c_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_and_package_limited_output_should_match_1(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}{filters(foo)}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(
+ depthTestCase,
+ pattern,
+ asList(
+ "foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
+ " ... suppressed 2 lines",
+ " at org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest.(ThrowablePatternConverterTest.java:0)",
+ " Suppressed: foo.TestFriendlyException: r_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ " Caused by: foo.TestFriendlyException: r_s_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ "Caused by: foo.TestFriendlyException: r_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_c_s [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ " Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Caused by: foo.TestFriendlyException: r_c_c [localized]",
+ " ... suppressed 2 lines",
+ " ... 3 more",
+ "Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#depthTestCases")
+ void depth_and_package_limited_output_should_match_2(final DepthTestCase depthTestCase) {
+ final String pattern = String.format(
+ "%s{%d}{filters(org.apache)}%s",
+ patternPrefix, depthTestCase.maxLineCount, depthTestCase.separatorTestCase.patternAddendum);
+ assertStackTraceLines(
+ depthTestCase,
+ pattern,
+ asList(
+ "foo.TestFriendlyException: r [localized]",
+ " at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.(TestFriendlyException.java:0)",
+ " ...",
+ " Suppressed: foo.TestFriendlyException: r_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_s_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ " Caused by: foo.TestFriendlyException: r_s_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Caused by: foo.TestFriendlyException: r_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 2 more",
+ " Suppressed: foo.TestFriendlyException: r_c_s [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ " Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+ "Caused by: foo.TestFriendlyException: r_c_c [localized]",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+ " ... 3 more",
+ "Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]"));
+ }
}
- @Test
- public void testFullWithSuffix() {
- final String[] options = {"full", "suffix(test suffix)"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- Throwable parent;
- try {
- try {
- throw new NullPointerException("null pointer");
- } catch (final NullPointerException e) {
- throw new IllegalArgumentException("IllegalArgument", e);
+ abstract static class AbstractStackTraceTest {
+
+ final String patternPrefix;
+
+ AbstractStackTraceTest(final String patternPrefix) {
+ this.patternPrefix = patternPrefix;
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.apache.logging.log4j.core.pattern.ThrowablePatternConverterTest#separatorTestCases")
+ void none_output_should_be_empty(final SeparatorTestCase separatorTestCase) {
+ final String effectivePattern = patternPrefix + "{none}" + separatorTestCase.patternAddendum;
+ final String stackTrace = convert(effectivePattern);
+ assertThat(stackTrace).as("pattern=`%s`", effectivePattern).isEmpty();
+ }
+
+ void assertStackTraceLines(
+ @Nullable final DepthTestCase depthTestCase,
+ final String pattern,
+ final List expectedStackTraceLines) {
+ final String actualStackTrace = convert(pattern);
+ final int maxLineCount;
+ final String conversionEnding;
+ if (depthTestCase == null) {
+ maxLineCount = Integer.MAX_VALUE;
+ conversionEnding = NEWLINE;
+ } else {
+ maxLineCount = depthTestCase.maxLineCount;
+ conversionEnding = depthTestCase.separatorTestCase.conversionEnding;
}
- } catch (final IllegalArgumentException e) {
- parent = e;
+ final String expectedStackTrace = expectedStackTraceLines.stream()
+ .limit(maxLineCount)
+ .map(expectedStackTraceLine -> expectedStackTraceLine + conversionEnding)
+ .collect(Collectors.joining());
+ final String truncatedActualStackTrace = normalizeStackTrace(actualStackTrace, conversionEnding);
+ final String truncatedExpectedStackTrace = normalizeStackTrace(expectedStackTrace, conversionEnding);
+ assertThat(truncatedActualStackTrace)
+ .as("depthTestCase=%s, pattern=`%s`", depthTestCase, pattern)
+ .isEqualTo(truncatedExpectedStackTrace);
+ }
+
+ private static String normalizeStackTrace(final String stackTrace, final String conversionEnding) {
+ return stackTrace
+ // Normalize line numbers
+ .replaceAll("\\.java:[0-9]+\\)", ".java:0)")
+ // Normalize extended stack trace resource information for Java Standard library classes.
+ // We replace the `~[?:1.8.0_422]` suffix of such classes with `~[?:0]`.
+ .replaceAll(" ~\\[\\?:[^]]+](\\Q" + conversionEnding + "\\E|$)", " ~[?:0]$1");
}
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(
- everyLineEndsWith(result, "test suffix"),
- "Each line of full stack trace should end with the specified suffix");
}
- @Test
- public void testShortOptionWithSuffix() {
- final String packageName = "org.apache.logging.log4j.core.pattern.";
- final String[] options = {"short.className", "suffix(test suffix)"};
- final ThrowablePatternConverter converter = ThrowablePatternConverter.newInstance(null, options);
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- final LogEvent event = Log4jLogEvent.newBuilder() //
- .setLoggerName("testLogger") //
- .setLoggerFqcn(this.getClass().getName()) //
- .setLevel(Level.DEBUG) //
- .setMessage(new SimpleMessage("test exception")) //
- .setThrown(parent)
- .build();
- final StringBuilder sb = new StringBuilder();
- converter.format(event, sb);
- final String result = sb.toString();
- assertTrue(everyLineEndsWith(result, "test suffix"), "Each line should end with suffix");
+ private static String convert(final String pattern) {
+ final List patternFormatters = PATTERN_PARSER.parse(pattern, false, true, true);
+ assertThat(patternFormatters).hasSize(1);
+ final PatternFormatter patternFormatter = patternFormatters.get(0);
+ final LogEvent logEvent =
+ Log4jLogEvent.newBuilder().setThrown(EXCEPTION).setLevel(LEVEL).build();
+ final StringBuilder buffer = new StringBuilder();
+ patternFormatter.format(logEvent, buffer);
+ return buffer.toString();
}
}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableTest.java
index 76ddfd4004e..3b25dce8da6 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/ThrowableTest.java
@@ -16,41 +16,214 @@
*/
package org.apache.logging.log4j.core.pattern;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.assertj.core.api.Assertions.assertThat;
-import java.util.List;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.LoggerTest;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
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.BeforeEach;
-import org.junit.jupiter.api.Test;
+import org.apache.logging.log4j.core.util.StringBuilderWriter;
+import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
/**
- * Unit tests for {@code throwable} pattern.
+ * Unit tests for {@code throwable}, {@code rThrowable} and {@code xThrowable} pattern.
*/
-@LoggerContextSource("log4j-throwable.xml")
public class ThrowableTest {
- private ListAppender app;
- private Logger logger;
+ static Stream testConverter_dataSource() {
+ final String filters = "org.junit,org.apache.maven,sun.reflect,java.lang.reflect";
+ final Integer depth = 5;
+ return Stream.of(
+ // Throwable
+ Arguments.of("%ex", filters, null, null, null),
+ Arguments.of("%ex", null, depth, null, null),
+ Arguments.of("%ex", null, null, "I am suffix", "#"),
+ // RootThrowable
+ Arguments.of("%rEx", filters, null, null, null),
+ Arguments.of("%rEx", null, depth, null, null),
+ Arguments.of("%rEx", null, null, "I am suffix", "#"),
+ // ExtendedThrowable
+ Arguments.of("%xEx", filters, null, null, null),
+ Arguments.of("%xEx", null, depth, null, null),
+ Arguments.of("%xEx", null, null, "I am suffix", "#"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("testConverter_dataSource")
+ void testConverter(String exceptionPattern, String filters, Integer depth, String suffix, String lineSeparator) {
+ final String pattern = buildPattern(exceptionPattern, filters, depth, suffix, lineSeparator);
+ final ConfigurationBuilder configBuilder =
+ ConfigurationBuilderFactory.newConfigurationBuilder();
+
+ final String appenderName = "LIST";
+ final Configuration config = configBuilder
+ .add(configBuilder
+ .newAppender(appenderName, "List")
+ .add(configBuilder.newLayout("PatternLayout").addAttribute("pattern", pattern)))
+ .add(configBuilder.newRootLogger(Level.ALL).add(configBuilder.newAppenderRef(appenderName)))
+ .build(false);
+
+ try (final LoggerContext loggerContext = Configurator.initialize(config)) {
+ // Restart logger context after first test run
+ if (loggerContext.isStopped()) {
+ loggerContext.start();
+ loggerContext.reconfigure(config);
+ }
+ final Throwable r = createException("r", 1, 3);
+
+ final Logger logger = loggerContext.getLogger(LoggerTest.class);
+ final ListAppender appender = loggerContext.getConfiguration().getAppender(appenderName);
+ logger.error("Exception", r);
+
+ assertThat(appender.getMessages()).hasSize(1);
+ final String message = appender.getMessages().get(0);
+ assertThat(message).isNotNull();
+ verifyFilters(message, filters);
+ verifyDepth(message, depth);
+ verifySuffix(message, suffix, lineSeparator);
+ }
+ }
+
+ static Stream renderers_dataSource() {
+ return Stream.of(
+ Arguments.of(new ThrowableStackTraceRenderer<>(Collections.emptyList(), Integer.MAX_VALUE)),
+ Arguments.of(new ThrowableInvertedStackTraceRenderer(Collections.emptyList(), Integer.MAX_VALUE)),
+ Arguments.of(new ThrowableExtendedStackTraceRenderer(Collections.emptyList(), Integer.MAX_VALUE)));
+ }
+
+ @ParameterizedTest
+ @MethodSource("renderers_dataSource")
+ void testCircularSuppressedExceptions(final ThrowableStackTraceRenderer> renderer) {
+ final Exception e1 = new Exception();
+ final Exception e2 = new Exception();
+ e2.addSuppressed(e1);
+ e1.addSuppressed(e2);
+
+ render(renderer, e1);
+ }
- @BeforeEach
- public void setUp(final LoggerContext context, @Named("List") final ListAppender app) {
- this.logger = context.getLogger("LoggerTest");
- this.app = app.clear();
+ @ParameterizedTest
+ @MethodSource("renderers_dataSource")
+ void testCircularSuppressedNestedException(final ThrowableStackTraceRenderer> renderer) {
+ final Exception e1 = new Exception();
+ final Exception e2 = new Exception(e1);
+ e2.addSuppressed(e1);
+ e1.addSuppressed(e2);
+
+ render(renderer, e1);
+ }
+
+ @ParameterizedTest
+ @MethodSource("renderers_dataSource")
+ void testCircularCauseExceptions(final ThrowableStackTraceRenderer> renderer) {
+ final Exception e1 = new Exception();
+ final Exception e2 = new Exception(e1);
+ e1.initCause(e2);
+ render(renderer, e1);
}
+ /**
+ * Default setting ThrowableRenderer render output should equal to throwable.printStackTrace().
+ */
@Test
- public void testException() {
- final Throwable cause = new NullPointerException("null pointer");
- final Throwable parent = new IllegalArgumentException("IllegalArgument", cause);
- logger.error("Exception", parent);
- final List msgs = app.getMessages();
- assertNotNull(msgs);
- assertEquals(1, msgs.size(), "Incorrect number of messages. Should be 1 is " + msgs.size());
- assertFalse(msgs.get(0).contains("suppressed"), "No suppressed lines");
+ public void testThrowableRenderer() {
+ final Throwable throwable = createException("r", 1, 3);
+ final ThrowableStackTraceRenderer> renderer =
+ new ThrowableStackTraceRenderer<>(Collections.emptyList(), Integer.MAX_VALUE);
+ String actual = render(renderer, throwable);
+ assertThat(actual).isEqualTo(getStandardThrowableStackTrace(throwable));
+ }
+
+ private static String render(final ThrowableStackTraceRenderer> renderer, final Throwable throwable) {
+ final StringBuilder stringBuilder = new StringBuilder();
+ renderer.renderThrowable(stringBuilder, throwable, System.lineSeparator());
+ return stringBuilder.toString();
+ }
+
+ private static String getStandardThrowableStackTrace(final Throwable throwable) {
+ final StringBuilder buffer = new StringBuilder();
+ final PrintWriter printWriter = new PrintWriter(new StringBuilderWriter(buffer));
+ throwable.printStackTrace(printWriter);
+ return buffer.toString();
+ }
+
+ private static String buildPattern(
+ final String exceptionPattern,
+ final String filters,
+ final Integer depth,
+ final String suffix,
+ final String lineSeparator) {
+ final StringBuilder buffer = new StringBuilder("%m");
+ buffer.append(exceptionPattern);
+ if (filters != null) {
+ buffer.append("{filters(");
+ buffer.append(filters);
+ buffer.append(")}");
+ }
+
+ if (depth != null) {
+ buffer.append("{");
+ buffer.append(depth);
+ buffer.append("}");
+ }
+
+ if (suffix != null) {
+ buffer.append("{suffix(");
+ buffer.append(suffix);
+ buffer.append(")}");
+ }
+
+ if (lineSeparator != null) {
+ buffer.append("{separator(");
+ buffer.append(lineSeparator);
+ buffer.append(")}");
+ }
+ return buffer.toString();
+ }
+
+ private static void verifyFilters(final String message, final String filters) {
+ if (filters != null) {
+ assertThat(message).contains("suppressed");
+ final String[] filterArray = filters.split(",");
+ for (final String filter : filterArray) {
+ assertThat(message).doesNotContain(filter);
+ }
+ } else {
+ assertThat(message).doesNotContain("suppressed");
+ }
+ }
+
+ private static void verifyDepth(final String message, final Integer depth) {
+ if (depth != null) {
+ assertThat(message).hasLineCount(depth);
+ }
+ }
+
+ private static void verifySuffix(final String message, final String suffix, final String lineSeparator) {
+ if (suffix != null && lineSeparator != null) {
+ for (String line : message.split(lineSeparator)) {
+ assertThat(line).endsWith(suffix);
+ }
+ }
+ }
+
+ private static Throwable createException(final String name, int depth, int maxDepth) {
+ Exception r = new Exception(name);
+ if (depth < maxDepth) {
+ r.initCause(createException(name + "_c", depth + 1, maxDepth));
+ r.addSuppressed(createException(name + "_s", depth + 1, maxDepth));
+ }
+ return r;
}
}
diff --git a/log4j-core-test/src/test/resources/log4j-rootthrowablefilter.xml b/log4j-core-test/src/test/resources/log4j-rootthrowablefilter.xml
deleted file mode 100644
index 9412a1e7760..00000000000
--- a/log4j-core-test/src/test/resources/log4j-rootthrowablefilter.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
- org.junit,org.apache.maven,sun.reflect,java.lang.reflect
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/log4j-core-test/src/test/resources/log4j-throwable.xml b/log4j-core-test/src/test/resources/log4j-throwable.xml
deleted file mode 100644
index db9647f9ef2..00000000000
--- a/log4j-core-test/src/test/resources/log4j-throwable.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
- org.junit,org.apache.maven,sun.reflect,java.lang.reflect
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/log4j-core-test/src/test/resources/log4j-throwablefilter.xml b/log4j-core-test/src/test/resources/log4j-throwablefilter.xml
deleted file mode 100644
index e1c97c03ad4..00000000000
--- a/log4j-core-test/src/test/resources/log4j-throwablefilter.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
- org.junit,org.apache.maven,sun.reflect,java.lang.reflect
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedClassInfo.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedClassInfo.java
index 73e1f18a2b3..2237b1ec31a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedClassInfo.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedClassInfo.java
@@ -24,6 +24,7 @@
/**
* Class and package data used with a {@link StackTraceElement} in a {@link ExtendedStackTraceElement}.
*/
+@Deprecated
public final class ExtendedClassInfo implements Serializable {
private static final long serialVersionUID = 1L;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedStackTraceElement.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedStackTraceElement.java
index 0301e440972..134ad90f14a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedStackTraceElement.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ExtendedStackTraceElement.java
@@ -32,6 +32,7 @@
* version
*
*/
+@Deprecated
public final class ExtendedStackTraceElement implements Serializable {
static final ExtendedStackTraceElement[] EMPTY_ARRAY = {};
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java
index fc5417911f1..5df74e5d9cd 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableFormatOptions.java
@@ -189,7 +189,7 @@ public boolean anyLines() {
* @return The number of lines to print.
*/
public int minLines(final int maxLines) {
- return this.lines > maxLines ? maxLines : this.lines;
+ return Math.min(this.lines, maxLines);
}
/**
@@ -267,13 +267,13 @@ public static ThrowableFormatOptions newInstance(String[] options) {
separator = option.substring("separator(".length(), option.length() - 1);
} else if (option.startsWith("filters(") && option.endsWith(")")) {
final String filterStr = option.substring("filters(".length(), option.length() - 1);
- if (filterStr.length() > 0) {
+ if (!filterStr.isEmpty()) {
final String[] array = filterStr.split(Patterns.COMMA_SEPARATOR);
if (array.length > 0) {
packages = new ArrayList<>(array.length);
for (String token : array) {
token = token.trim();
- if (token.length() > 0) {
+ if (!token.isEmpty()) {
packages.add(token);
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java
index 8685294a54f..61d292dc487 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxy.java
@@ -47,6 +47,7 @@
* TODO: Deserialize: Try to rebuild Throwable if the target exception is in this class loader?
*
*/
+@Deprecated
public class ThrowableProxy implements Serializable {
private static final char EOL = Chars.LF;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyHelper.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyHelper.java
index 504bc6a11f9..d35fc881275 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyHelper.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyHelper.java
@@ -32,6 +32,7 @@
* {@link ThrowableProxyHelper} provides utilities required to initialize a new {@link ThrowableProxy}
* instance.
*/
+@Deprecated
final class ThrowableProxyHelper {
private ThrowableProxyHelper() {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyRenderer.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyRenderer.java
index 79994753dc9..431442095bf 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyRenderer.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThrowableProxyRenderer.java
@@ -24,6 +24,7 @@
* {@link ThrowableProxyRenderer} is an internal utility providing the code to render a {@link ThrowableProxy}
* to a {@link StringBuilder}.
*/
+@Deprecated
final class ThrowableProxyRenderer {
private static final String TAB = "\t";
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 0c3b08f43a7..2d4bdcd199c 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.24.0")
+@Version("2.25.0")
package org.apache.logging.log4j.core.impl;
import org.osgi.annotation.bundle.Export;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedStackTraceElementMixIn.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedStackTraceElementMixIn.java
index 13fbaede617..1ddd00d0a2e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedStackTraceElementMixIn.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedStackTraceElementMixIn.java
@@ -39,6 +39,7 @@
ExtendedStackTraceElementMixIn.ATTR_VERSION
// @formatter:on
})
+@Deprecated
abstract class ExtendedStackTraceElementMixIn implements Serializable {
protected static final String ATTR_CLASS_LOADER_NAME = StackTraceElementConstants.ATTR_CLASS_LOADER_NAME;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyMixIn.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyMixIn.java
index 4b2b4292a1c..44996a3ade7 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyMixIn.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyMixIn.java
@@ -26,6 +26,7 @@
/**
* Mix-in for {@link ThrowableProxy}.
*/
+@Deprecated
abstract class ThrowableProxyMixIn {
@JsonProperty(JsonConstants.ELT_CAUSE)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithStacktraceAsStringMixIn.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithStacktraceAsStringMixIn.java
index 3400a8c1070..eda9bcf32f2 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithStacktraceAsStringMixIn.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithStacktraceAsStringMixIn.java
@@ -26,6 +26,7 @@
/**
* Mix-in for {@link org.apache.logging.log4j.core.impl.ThrowableProxy}.
*/
+@Deprecated
abstract class ThrowableProxyWithStacktraceAsStringMixIn {
@JsonProperty(JsonConstants.ELT_CAUSE)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithoutStacktraceMixIn.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithoutStacktraceMixIn.java
index b7590015365..2ea10d3e1d1 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithoutStacktraceMixIn.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/jackson/ThrowableProxyWithoutStacktraceMixIn.java
@@ -26,6 +26,7 @@
/**
* Mix-in for {@link ThrowableProxy}.
*/
+@Deprecated
abstract class ThrowableProxyWithoutStacktraceMixIn {
@JsonProperty(JsonConstants.ELT_CAUSE)
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
index 76e38aaad60..a915cdadfbc 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
@@ -259,7 +259,7 @@ private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
*/
public static PatternParser createPatternParser(final Configuration config) {
if (config == null) {
- return new PatternParser(config, KEY, LogEventPatternConverter.class);
+ return new PatternParser(null, KEY, LogEventPatternConverter.class);
}
PatternParser parser = config.getComponent(KEY);
if (parser == null) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ClassResourceInfo.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ClassResourceInfo.java
new file mode 100644
index 00000000000..a01d1bc5e63
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ClassResourceInfo.java
@@ -0,0 +1,92 @@
+/*
+ * 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.pattern;
+
+import java.net.URL;
+import java.security.CodeSource;
+
+/**
+ * Resource information (i.e., the enclosing JAR file and its version) of a class.
+ */
+final class ClassResourceInfo {
+
+ static final ClassResourceInfo UNKNOWN = new ClassResourceInfo();
+
+ private final String text;
+
+ final Class> clazz;
+
+ /**
+ * Constructs an instance modelling an unknown class resource.
+ */
+ private ClassResourceInfo() {
+ this.text = "~[?:?]";
+ this.clazz = null;
+ }
+
+ /**
+ * @param clazz the class
+ * @param exact {@code true}, if the class was obtained via reflection; {@code false}, otherwise
+ */
+ ClassResourceInfo(final Class> clazz, final boolean exact) {
+ this.clazz = clazz;
+ this.text = getText(clazz, exact);
+ }
+
+ private static String getText(final Class> clazz, final boolean exact) {
+ final String exactnessPrefix = exact ? "" : "~";
+ final String location = getLocation(clazz);
+ final String version = getVersion(clazz);
+ return String.format("%s[%s:%s]", exactnessPrefix, location, version);
+ }
+
+ private static String getLocation(final Class> clazz) {
+ try {
+ final CodeSource source = clazz.getProtectionDomain().getCodeSource();
+ if (source != null) {
+ final URL locationUrl = source.getLocation();
+ if (locationUrl != null) {
+ final String normalizedLocationUrl = locationUrl.toString().replace('\\', '/');
+ int separatorIndex = normalizedLocationUrl.lastIndexOf("/");
+ if (separatorIndex >= 0 && separatorIndex == normalizedLocationUrl.length() - 1) {
+ separatorIndex = normalizedLocationUrl.lastIndexOf("/", separatorIndex - 1);
+ }
+ return normalizedLocationUrl.substring(separatorIndex + 1);
+ }
+ }
+ } catch (final Exception ignored) {
+ // Do nothing
+ }
+ return "?";
+ }
+
+ private static String getVersion(final Class> clazz) {
+ final Package classPackage = clazz.getPackage();
+ if (classPackage != null) {
+ final String version = classPackage.getImplementationVersion();
+ if (version != null) {
+ return version;
+ }
+ }
+ return "?";
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java
index 12e362e8d15..4b138d202ac 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ExtendedThrowablePatternConverter.java
@@ -16,67 +16,37 @@
*/
package org.apache.logging.log4j.core.pattern;
-import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
-import org.apache.logging.log4j.core.impl.ThrowableProxy;
+import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
- * Outputs the Throwable portion of the LoggingEvent as a full stack trace
- * unless this converter's option is 'short', where it just outputs the first line of the trace, or if
- * the number of lines to print is explicitly specified.
- *
- * The extended stack trace will also include the location of where the class was loaded from and the
- * version of the jar if available.
+ * {@link ThrowablePatternConverter} variant where the rendered {@link StackTraceElement}s are enriched with the enclosing JAR file and its version information, if available.
*/
+@NullMarked
@Plugin(name = "ExtendedThrowablePatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"xEx", "xThrowable", "xException"})
public final class ExtendedThrowablePatternConverter extends ThrowablePatternConverter {
- /**
- * Private constructor.
- *
- * @param config
- * @param options options, may be null.
- */
- private ExtendedThrowablePatternConverter(final Configuration config, final String[] options) {
- super("ExtendedThrowable", "throwable", options, config);
+ private ExtendedThrowablePatternConverter(@Nullable final Configuration config, @Nullable final String[] options) {
+ super("ExtendedThrowable", "throwable", options, config, ExtendedThrowablePatternConverter::createRenderer);
}
- /**
- * Gets an instance of the class.
- *
- * @param config The current Configuration.
- * @param options pattern options, may be null. If first element is "short",
- * only the first line of the throwable will be formatted.
- * @return instance of class.
- */
- public static ExtendedThrowablePatternConverter newInstance(final Configuration config, final String[] options) {
- return new ExtendedThrowablePatternConverter(config, options);
+ private static ThrowableExtendedStackTraceRenderer createRenderer(final ThrowableFormatOptions options) {
+ return new ThrowableExtendedStackTraceRenderer(options.getIgnorePackages(), options.getLines());
}
/**
- * {@inheritDoc}
+ * Creates an instance of the class.
+ *
+ * @param config a configuration
+ * @param options the pattern options
+ * @return a new instance
*/
- @Override
- public void format(final LogEvent event, final StringBuilder toAppendTo) {
- final ThrowableProxy proxy = event.getThrownProxy();
- final Throwable throwable = event.getThrown();
- if ((throwable != null || proxy != null) && options.anyLines()) {
- if (proxy == null) {
- super.format(event, toAppendTo);
- return;
- }
- final int len = toAppendTo.length();
- if (len > 0 && !Character.isWhitespace(toAppendTo.charAt(len - 1))) {
- toAppendTo.append(' ');
- }
- proxy.formatExtendedStackTraceTo(
- toAppendTo,
- options.getIgnorePackages(),
- options.getTextRenderer(),
- getSuffix(event),
- options.getSeparator());
- }
+ public static ExtendedThrowablePatternConverter newInstance(
+ @Nullable final Configuration config, @Nullable final String[] options) {
+ return new ExtendedThrowablePatternConverter(config, options);
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java
index 9b2e54ca16d..713f79acc36 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/PatternParser.java
@@ -212,7 +212,7 @@ public List parse(
list.add(new PatternFormatter(pc, field));
}
if (alwaysWriteExceptions && !handlesThrowable) {
- final LogEventPatternConverter pc = ExtendedThrowablePatternConverter.newInstance(config, null);
+ final LogEventPatternConverter pc = ExtendedThrowablePatternConverter.newInstance(config, new String[0]);
list.add(new PatternFormatter(pc, FormattingInfo.getDefault()));
}
return list;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java
index a068c3d7601..c273e6a82b5 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RootThrowablePatternConverter.java
@@ -16,79 +16,37 @@
*/
package org.apache.logging.log4j.core.pattern;
-import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
-import org.apache.logging.log4j.core.impl.ThrowableProxy;
-import org.apache.logging.log4j.util.Strings;
+import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
- * Outputs the Throwable portion of the LoggingEvent as a full stack trace
- * unless this converter's option is 'short', where it just outputs the first line of the trace, or if
- * the number of lines to print is explicitly specified.
- *
- * The extended stack trace will also include the location of where the class was loaded from and the
- * version of the jar if available.
+ * {@link ThrowablePatternConverter} variant where the stack trace causal chain is rendered in reverse order.
*/
+@NullMarked
@Plugin(name = "RootThrowablePatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"rEx", "rThrowable", "rException"})
public final class RootThrowablePatternConverter extends ThrowablePatternConverter {
- /**
- * Private constructor.
- *
- * @param config the Configuration or {@code null}
- * @param options Options, may be null.
- */
- private RootThrowablePatternConverter(final Configuration config, final String[] options) {
- super("RootThrowable", "throwable", options, config);
+ private RootThrowablePatternConverter(@Nullable final Configuration config, @Nullable final String[] options) {
+ super("RootThrowable", "throwable", options, config, RootThrowablePatternConverter::createRenderer);
}
- /**
- * Gets an instance of the class.
- *
- * @param config The Configuration or {@code null}.
- * @param options pattern options, may be null. If first element is "short",
- * only the first line of the throwable will be formatted.
- * @return instance of class.
- */
- public static RootThrowablePatternConverter newInstance(final Configuration config, final String[] options) {
- return new RootThrowablePatternConverter(config, options);
+ private static ThrowableInvertedStackTraceRenderer createRenderer(final ThrowableFormatOptions options) {
+ return new ThrowableInvertedStackTraceRenderer(options.getIgnorePackages(), options.getLines());
}
/**
- * {@inheritDoc}
+ * Creates an instance of the class.
+ *
+ * @param config a configuration
+ * @param options the pattern options
+ * @return a new instance
*/
- @Override
- public void format(final LogEvent event, final StringBuilder toAppendTo) {
- final ThrowableProxy proxy = event.getThrownProxy();
- final Throwable throwable = event.getThrown();
- if (throwable != null && options.anyLines()) {
- if (proxy == null) {
- super.format(event, toAppendTo);
- return;
- }
- final String trace = proxy.getCauseStackTraceAsString(
- options.getIgnorePackages(), options.getTextRenderer(), getSuffix(event), options.getSeparator());
- final int len = toAppendTo.length();
- if (len > 0 && !Character.isWhitespace(toAppendTo.charAt(len - 1))) {
- toAppendTo.append(' ');
- }
- if (!options.allLines() || !Strings.LINE_SEPARATOR.equals(options.getSeparator())) {
- final StringBuilder sb = new StringBuilder();
- final String[] array = trace.split(Strings.LINE_SEPARATOR);
- final int limit = options.minLines(array.length) - 1;
- for (int i = 0; i <= limit; ++i) {
- sb.append(array[i]);
- if (i < limit) {
- sb.append(options.getSeparator());
- }
- }
- toAppendTo.append(sb.toString());
-
- } else {
- toAppendTo.append(trace);
- }
- }
+ public static RootThrowablePatternConverter newInstance(
+ @Nullable final Configuration config, @Nullable final String[] options) {
+ return new RootThrowablePatternConverter(config, options);
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRenderer.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRenderer.java
new file mode 100644
index 00000000000..01d804a3da6
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableExtendedStackTraceRenderer.java
@@ -0,0 +1,193 @@
+/*
+ * 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.pattern;
+
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.util.LoaderUtil;
+import org.apache.logging.log4j.util.StackLocatorUtil;
+
+/**
+ * {@link ThrowableStackTraceRenderer} variant where the rendered {@link StackTraceElement}s are enriched with the enclosing JAR file and its version information, if available.
+ */
+final class ThrowableExtendedStackTraceRenderer
+ extends ThrowableStackTraceRenderer {
+
+ ThrowableExtendedStackTraceRenderer(final List ignoredPackageNames, final int maxLineCount) {
+ super(ignoredPackageNames, maxLineCount);
+ }
+
+ @Override
+ ExtendedContext createContext(final Throwable throwable) {
+ return ExtendedContext.ofThrowable(throwable);
+ }
+
+ @Override
+ void renderStackTraceElement(
+ final StringBuilder buffer,
+ final StackTraceElement stackTraceElement,
+ final ExtendedContext context,
+ final String prefix,
+ final String lineSeparator) {
+
+ // Short-circuit on ignored stack trace elements
+ final boolean stackTraceElementIgnored = isStackTraceElementIgnored(stackTraceElement);
+ if (stackTraceElementIgnored) {
+ context.ignoredStackTraceElementCount += 1;
+ return;
+ }
+
+ // Render the suppressed stack trace element count
+ if (context.ignoredStackTraceElementCount > 0) {
+ renderSuppressedCount(buffer, context, prefix, lineSeparator);
+ context.ignoredStackTraceElementCount = 0;
+ }
+
+ // Render the stack trace element
+ acquireLineCapacity(context);
+ buffer.append(prefix);
+ buffer.append("\tat ");
+ buffer.append(stackTraceElement);
+ final ClassResourceInfo classResourceInfo =
+ context.classResourceInfoByName.get(stackTraceElement.getClassName());
+ if (classResourceInfo != null) {
+ buffer.append(' ');
+ buffer.append(classResourceInfo);
+ }
+ buffer.append(lineSeparator);
+ }
+
+ static final class ExtendedContext extends ThrowableStackTraceRenderer.Context {
+
+ private final Map classResourceInfoByName;
+
+ private ExtendedContext(
+ final int ignoredStackTraceElementCount,
+ final Map metadataByThrowable,
+ final Map classResourceInfoByName) {
+ super(ignoredStackTraceElementCount, metadataByThrowable);
+ this.classResourceInfoByName = classResourceInfoByName;
+ }
+
+ private static ExtendedContext ofThrowable(final Throwable throwable) {
+ final Map metadataByThrowable = Metadata.ofThrowable(throwable);
+ final Map classResourceInfoByName =
+ createClassResourceInfoByName(throwable, metadataByThrowable);
+ return new ExtendedContext(0, metadataByThrowable, classResourceInfoByName);
+ }
+
+ private static Map createClassResourceInfoByName(
+ final Throwable rootThrowable, final Map metadataByThrowable) {
+
+ // Stack trace elements of a `Throwable` only contain the class name.
+ // But we need the associated `Class>` to extract its resource information, i.e., JAR file and version.
+ // We are capturing the current stack to find suitable class loaders.
+ // We will use this as a bootstrap to go from a class name in a stack trace to a `Class>`.
+ final Map classResourceInfoByName =
+ StackLocatorUtil.getCurrentStackTrace().stream()
+ .collect(Collectors.toMap(
+ Class::getName,
+ clazz -> new ClassResourceInfo(clazz, true),
+ (classResourceInfo1, classResourceInfo2) -> classResourceInfo1));
+
+ // Walk over the causal chain
+ final Set visitedThrowables = new HashSet<>();
+ final Queue pendingThrowables = new ArrayDeque<>(Collections.singleton(rootThrowable));
+ Throwable throwable;
+ while ((throwable = pendingThrowables.poll()) != null && visitedThrowables.add(throwable)) {
+
+ // Add the cause to the processing queue
+ final Throwable cause = throwable.getCause();
+ if (cause != null) {
+ pendingThrowables.offer(cause);
+ }
+
+ // Short-circuit if there are no associated metadata
+ final Metadata metadata = metadataByThrowable.get(throwable);
+ if (metadata == null) {
+ continue;
+ }
+
+ ClassLoader lastLoader = null;
+ final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+ for (int throwableStackIndex = metadata.stackLength - 1;
+ throwableStackIndex >= 0;
+ --throwableStackIndex) {
+
+ // Skip if the current class name is either known, or already visited and is unknown
+ final StackTraceElement stackTraceElement = stackTraceElements[throwableStackIndex];
+ final String stackTraceElementClassName = stackTraceElement.getClassName();
+ ClassResourceInfo classResourceInfo = classResourceInfoByName.get(stackTraceElementClassName);
+ if (classResourceInfo != null) {
+ if (classResourceInfo.clazz != null) {
+ lastLoader = classResourceInfo.clazz.getClassLoader();
+ }
+ continue;
+ }
+
+ // Try to determine the stack trace element class, and register the result to the lookup table
+ final Class> stackTraceElementClass = loadClass(lastLoader, stackTraceElementClassName);
+ classResourceInfo = stackTraceElementClass != null
+ ? new ClassResourceInfo(stackTraceElementClass, false)
+ : ClassResourceInfo.UNKNOWN;
+ classResourceInfoByName.put(stackTraceElementClassName, classResourceInfo);
+ }
+ }
+ return classResourceInfoByName;
+ }
+
+ @FunctionalInterface
+ private interface ThrowingSupplier {
+
+ V supply() throws Exception;
+ }
+
+ private static Class> loadClass(final ClassLoader loader, final String className) {
+ return Stream.>>of(
+ // 1. Try the passed class loader
+ () -> loader != null ? loader.loadClass(className) : null,
+ // 2. Try the `LoaderUtil` magic
+ () -> LoaderUtil.loadClass(className),
+ // 3. Try the current class loader
+ () -> ThrowableExtendedStackTraceRenderer.class
+ .getClassLoader()
+ .loadClass(className))
+ .map(provider -> {
+ try {
+ final Class> clazz = provider.supply();
+ if (clazz != null) {
+ return clazz;
+ }
+ } catch (final Exception ignored) {
+ // Do nothing
+ }
+ return null;
+ })
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
+ }
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java
new file mode 100644
index 00000000000..786016f5496
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableInvertedStackTraceRenderer.java
@@ -0,0 +1,131 @@
+/*
+ * 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.pattern;
+
+import java.util.List;
+import java.util.Set;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@link ThrowableStackTraceRenderer} variant where the stack trace causal chain is rendered in reverse order.
+ */
+final class ThrowableInvertedStackTraceRenderer
+ extends ThrowableStackTraceRenderer {
+
+ private static final String WRAPPED_BY_CAPTION = "Wrapped by: ";
+
+ ThrowableInvertedStackTraceRenderer(final List ignoredPackageNames, final int maxLineCount) {
+ super(ignoredPackageNames, maxLineCount);
+ }
+
+ @Override
+ void renderThrowable(
+ final StringBuilder buffer,
+ final Throwable throwable,
+ final Context context,
+ final Set visitedThrowables,
+ final String lineSeparator) {
+ renderThrowable(buffer, throwable, context, visitedThrowables, "", lineSeparator, false);
+ }
+
+ private void renderThrowable(
+ final StringBuilder buffer,
+ final Throwable throwable,
+ final Context context,
+ final Set visitedThrowables,
+ final String prefix,
+ final String lineSeparator,
+ boolean lineCapacityAcquired) {
+ final boolean circular = !visitedThrowables.add(throwable);
+ if (circular) {
+ if (!lineCapacityAcquired) {
+ acquireLineCapacity(context);
+ }
+ buffer.append("[CIRCULAR REFERENCE: ");
+ renderThrowableMessage(buffer, throwable);
+ buffer.append(']');
+ buffer.append(lineSeparator);
+ } else {
+ lineCapacityAcquired = renderCause(
+ buffer,
+ throwable.getCause(),
+ context,
+ visitedThrowables,
+ prefix,
+ lineSeparator,
+ lineCapacityAcquired);
+ if (!lineCapacityAcquired) {
+ acquireLineCapacity(context);
+ }
+ renderThrowableMessage(buffer, throwable);
+ buffer.append(lineSeparator);
+ renderStackTraceElements(buffer, throwable, context, prefix, lineSeparator);
+ renderSuppressed(
+ buffer, throwable.getSuppressed(), context, visitedThrowables, prefix + '\t', lineSeparator);
+ }
+ }
+
+ /**
+ * @return {@code true}, if line capacity is acquired; {@code false}, otherwise
+ */
+ private boolean renderCause(
+ final StringBuilder buffer,
+ @Nullable final Throwable cause,
+ final Context context,
+ final Set visitedThrowables,
+ final String prefix,
+ final String lineSeparator,
+ final boolean lineCapacityAcquired) {
+ if (cause != null) {
+ renderThrowable(buffer, cause, context, visitedThrowables, prefix, lineSeparator, lineCapacityAcquired);
+ acquireLineCapacity(context);
+ buffer.append(prefix);
+ buffer.append(WRAPPED_BY_CAPTION);
+ return true;
+ }
+ return lineCapacityAcquired;
+ }
+
+ @Override
+ void renderSuppressed(
+ final StringBuilder buffer,
+ final Throwable[] suppressedThrowables,
+ final Context context,
+ final Set visitedThrowables,
+ final String prefix,
+ final String lineSeparator) {
+ if (suppressedThrowables.length > 0) {
+ acquireLineCapacity(context);
+ buffer.append(prefix);
+ buffer.append(SUPPRESSED_CAPTION);
+ for (int suppressedThrowableIndex = 0;
+ suppressedThrowableIndex < suppressedThrowables.length;
+ suppressedThrowableIndex++) {
+ final Throwable suppressedThrowable = suppressedThrowables[suppressedThrowableIndex];
+ final boolean lineCapacityAcquired = suppressedThrowableIndex == 0;
+ renderThrowable(
+ buffer,
+ suppressedThrowable,
+ context,
+ visitedThrowables,
+ prefix,
+ lineSeparator,
+ lineCapacityAcquired);
+ }
+ }
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java
index d2086cbc212..d7c2060fb7b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.java
@@ -16,112 +16,113 @@
*/
package org.apache.logging.log4j.core.pattern;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import java.io.PrintWriter;
-import java.io.StringWriter;
+import static java.util.Objects.requireNonNull;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.function.Function;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
import org.apache.logging.log4j.core.layout.PatternLayout;
-import org.apache.logging.log4j.core.util.StringBuilderWriter;
-import org.apache.logging.log4j.util.Strings;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
- * Outputs the Throwable portion of the LoggingEvent as a full stack trace
- * unless this converter's option is 'short', where it just outputs the first line of the trace, or if
- * the number of lines to print is explicitly specified.
+ * Outputs certain information extracted from the {@link Throwable} associated with a {@link LogEvent}.
*/
+@NullMarked
@Plugin(name = "ThrowablePatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"ex", "throwable", "exception"})
public class ThrowablePatternConverter extends LogEventPatternConverter {
/**
- * Lists {@link PatternFormatter}s for the suffix attribute.
+ * Returns the list of formatters used to render the suffix.
+ *
+ * @deprecated Kept for binary backward compatibility.
*/
+ @Deprecated
protected final List formatters;
- private String rawOption;
- private final boolean subShortOption;
- private final boolean nonStandardLineSeparator;
+ private final Function effectiveLineSeparatorProvider;
+
+ protected final ThrowableFormatOptions options;
+
+ private final ThrowableRenderer renderer;
/**
- * Options.
+ * @deprecated Use {@link #ThrowablePatternConverter(String, String, String[], Configuration, Function)} instead.
*/
- protected final ThrowableFormatOptions options;
+ @Deprecated
+ protected ThrowablePatternConverter(final String name, final String style, @Nullable final String[] options) {
+ this(name, style, options, null, null);
+ }
/**
- * Constructor.
- * @param name Name of converter.
- * @param style CSS style for output.
- * @param options options, may be null.
- * @deprecated Use ThrowablePatternConverter(String name, String stule, String[] options, Configuration config)
+ * @deprecated Use {@link #ThrowablePatternConverter(String, String, String[], Configuration, Function)} instead.
*/
@Deprecated
- protected ThrowablePatternConverter(final String name, final String style, final String[] options) {
- this(name, style, options, null);
+ protected ThrowablePatternConverter(
+ final String name,
+ final String style,
+ @Nullable final String[] options,
+ @Nullable final Configuration config) {
+ this(name, style, options, config, null);
}
/**
- * Constructor.
- * @param name name of converter
+ * The canonical constructor.
+ *
+ * @param name name of the converter
* @param style CSS style for output
- * @param options options, may be null.
- * @param config the Configuration or {@code null}
+ * @param options array of options
+ * @param config a configuration
+ * @param stackTraceRendererFactory a renderer factory
+ * @since 2.25.0
*/
- protected ThrowablePatternConverter(
- final String name, final String style, final String[] options, final Configuration config) {
+ ThrowablePatternConverter(
+ final String name,
+ final String style,
+ @Nullable final String[] options,
+ @Nullable final Configuration config,
+ @Nullable
+ final Function> stackTraceRendererFactory) {
+
+ // Process `name`, `style`, and `options`
super(name, style);
this.options = ThrowableFormatOptions.newInstance(options);
- if (options != null && options.length > 0) {
- rawOption = options[0];
- }
- if (this.options.getSuffix() != null) {
- final PatternParser parser = PatternLayout.createPatternParser(config);
- final List parsedSuffixFormatters = parser.parse(this.options.getSuffix());
- // filter out nested formatters that will handle throwable
- boolean hasThrowableSuffixFormatter = false;
- for (final PatternFormatter suffixFormatter : parsedSuffixFormatters) {
- if (suffixFormatter.handlesThrowable()) {
- hasThrowableSuffixFormatter = true;
- }
- }
- if (!hasThrowableSuffixFormatter) {
- this.formatters = parsedSuffixFormatters;
- } else {
- final List suffixFormatters = new ArrayList<>();
- for (final PatternFormatter suffixFormatter : parsedSuffixFormatters) {
- if (!suffixFormatter.handlesThrowable()) {
- suffixFormatters.add(suffixFormatter);
- }
- }
- this.formatters = suffixFormatters;
- }
+
+ // Determine the effective line separator
+ final List suffixFormatters = new ArrayList<>();
+ this.effectiveLineSeparatorProvider = createEffectiveLineSeparator(
+ this.options.getSeparator(), this.options.getSuffix(), config, suffixFormatters);
+ this.formatters = Collections.unmodifiableList(suffixFormatters);
+
+ // Create the effective renderer
+ final ThrowablePropertyRenderer propertyRenderer = ThrowablePropertyRenderer.fromOptions(options);
+ if (propertyRenderer != null) {
+ this.renderer = propertyRenderer;
} else {
- this.formatters = Collections.emptyList();
+ final Function> effectiveRendererFactory =
+ stackTraceRendererFactory != null
+ ? stackTraceRendererFactory
+ : ThrowablePatternConverter::createRenderer;
+ this.renderer = effectiveRendererFactory.apply(this.options);
}
- subShortOption = ThrowableFormatOptions.MESSAGE.equalsIgnoreCase(rawOption)
- || ThrowableFormatOptions.LOCALIZED_MESSAGE.equalsIgnoreCase(rawOption)
- || ThrowableFormatOptions.FILE_NAME.equalsIgnoreCase(rawOption)
- || ThrowableFormatOptions.LINE_NUMBER.equalsIgnoreCase(rawOption)
- || ThrowableFormatOptions.METHOD_NAME.equalsIgnoreCase(rawOption)
- || ThrowableFormatOptions.CLASS_NAME.equalsIgnoreCase(rawOption);
- nonStandardLineSeparator = !Strings.LINE_SEPARATOR.equals(this.options.getSeparator());
}
/**
- * Gets an instance of the class.
+ * Creates an instance of the class.
*
- * @param config The Configuration or {@code null}.
- * @param options pattern options, may be null. If first element is "short",
- * only the first line of the throwable will be formatted.
- * @return instance of class.
+ * @param config a configuration
+ * @param options the pattern options
+ * @return a new instance
*/
- public static ThrowablePatternConverter newInstance(final Configuration config, final String[] options) {
- return new ThrowablePatternConverter("Throwable", "throwable", options, config);
+ public static ThrowablePatternConverter newInstance(
+ @Nullable final Configuration config, @Nullable final String[] options) {
+ return new ThrowablePatternConverter("Throwable", "throwable", options, config, null);
}
/**
@@ -129,110 +130,130 @@ public static ThrowablePatternConverter newInstance(final Configuration config,
*/
@Override
public void format(final LogEvent event, final StringBuilder buffer) {
- final Throwable t = event.getThrown();
+ requireNonNull(event, "event");
+ requireNonNull(buffer, "buffer");
+ final Throwable throwable = event.getThrown();
+ if (throwable != null) {
+ final String lineSeparator = effectiveLineSeparatorProvider.apply(event);
+ ensureWhitespaceSuffix(buffer);
+ renderer.renderThrowable(buffer, throwable, lineSeparator);
+ }
+ }
- if (subShortOption) {
- formatSubShortOption(t, getSuffix(event), buffer);
- } else if (t != null && options.anyLines()) {
- formatOption(t, getSuffix(event), buffer);
+ private static void ensureWhitespaceSuffix(final StringBuilder buffer) {
+ final int bufferLength = buffer.length();
+ if (bufferLength > 0 && !Character.isWhitespace(buffer.charAt(bufferLength - 1))) {
+ buffer.append(' ');
}
}
- private void formatSubShortOption(final Throwable t, final String suffix, final StringBuilder buffer) {
- StackTraceElement[] trace;
- StackTraceElement throwingMethod = null;
- int len;
+ /**
+ * Indicates this converter handles {@link Throwable}s.
+ *
+ * @return {@code true}
+ */
+ @Override
+ public boolean handlesThrowable() {
+ return true;
+ }
- if (t != null) {
- trace = t.getStackTrace();
- if (trace != null && trace.length > 0) {
- throwingMethod = trace[0];
- }
- }
+ public ThrowableFormatOptions getOptions() {
+ return options;
+ }
- if (t != null && throwingMethod != null) {
- String toAppend = Strings.EMPTY;
-
- if (ThrowableFormatOptions.CLASS_NAME.equalsIgnoreCase(rawOption)) {
- toAppend = throwingMethod.getClassName();
- } else if (ThrowableFormatOptions.METHOD_NAME.equalsIgnoreCase(rawOption)) {
- toAppend = throwingMethod.getMethodName();
- } else if (ThrowableFormatOptions.LINE_NUMBER.equalsIgnoreCase(rawOption)) {
- toAppend = String.valueOf(throwingMethod.getLineNumber());
- } else if (ThrowableFormatOptions.MESSAGE.equalsIgnoreCase(rawOption)) {
- toAppend = t.getMessage();
- } else if (ThrowableFormatOptions.LOCALIZED_MESSAGE.equalsIgnoreCase(rawOption)) {
- toAppend = t.getLocalizedMessage();
- } else if (ThrowableFormatOptions.FILE_NAME.equalsIgnoreCase(rawOption)) {
- toAppend = throwingMethod.getFileName();
- }
+ /**
+ * Creates a lambda that returns the effective line separator by concatenating the formatted {@code suffix} with the {@code separator}.
+ *
+ * At the beginning, there was only {@code separator} used as a terminator at the end of every rendered line.
+ * Its content was rendered literally without any processing.
+ *
+ *
+ * Later on, {@code suffix} was added in #61.
+ * {@code suffix} is functionally identical to {@code separator} with the exception that it contains a Pattern Layout conversion pattern.
+ * In an ideal world, {@code separator} should have been extended to accept patterns.
+ * But without giving it a second of thought, just like almost any other Log4j feature, we cheerfully accepted the feature.
+ *
+ *
+ * Given two overlapping features, how do we determine the effective line separator?
+ *
+ * {@code
+ * String effectiveLineSeparator(String separator, String suffix, LogEvent event) {
+ * String formattedSuffix = format(suffix, event);
+ * return isNotBlank(formattedSuffix)
+ * ? (' ' + formattedSuffix + lineSeparator)
+ * : lineSeparator;
+ * }
+ * }
+ *
+ * @param separator the user-provided {@code separator} option
+ * @param suffix the user-provided {@code suffix} option containing a Pattern Layout conversion pattern
+ * @param config the configuration to create the Pattern Layout conversion pattern parser
+ * @param suffixFormatters the list of pattern formatters employed to format the suffix
+ * @return a lambda that returns the effective line separator by concatenating the formatted {@code suffix} with the {@code separator}
+ */
+ private static Function createEffectiveLineSeparator(
+ final String separator,
+ @Nullable final String suffix,
+ @Nullable final Configuration config,
+ final List suffixFormatters) {
+ requireNonNull(separator, "separator");
+ requireNonNull(suffixFormatters, "suffixFormatters");
+ if (suffix != null) {
- len = buffer.length();
- if (len > 0 && !Character.isWhitespace(buffer.charAt(len - 1))) {
- buffer.append(' ');
- }
- buffer.append(toAppend);
+ // Suffix is allowed to be a Pattern Layout conversion pattern, hence we need to parse it
+ final PatternParser parser = PatternLayout.createPatternParser(config);
+ final List parsedSuffixFormatters = parser.parse(suffix);
- if (Strings.isNotBlank(suffix)) {
- buffer.append(' ');
- buffer.append(suffix);
+ // Collect formatters excluding ones handling throwables
+ for (final PatternFormatter suffixFormatter : parsedSuffixFormatters) {
+ if (!suffixFormatter.handlesThrowable()) {
+ suffixFormatters.add(suffixFormatter);
+ }
}
- }
- }
- @SuppressFBWarnings(
- value = "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE",
- justification = "Formatting a throwable is the main purpose of this class.")
- private void formatOption(final Throwable throwable, final String suffix, final StringBuilder buffer) {
- final int len = buffer.length();
- if (len > 0 && !Character.isWhitespace(buffer.charAt(len - 1))) {
- buffer.append(' ');
- }
- if (!options.allLines() || nonStandardLineSeparator || Strings.isNotBlank(suffix)) {
- final StringWriter w = new StringWriter();
- throwable.printStackTrace(new PrintWriter(w));
-
- final String[] array = w.toString().split(Strings.LINE_SEPARATOR);
- final int limit = options.minLines(array.length) - 1;
- final boolean suffixNotBlank = Strings.isNotBlank(suffix);
- for (int i = 0; i <= limit; ++i) {
- buffer.append(array[i]);
- if (suffixNotBlank) {
- buffer.append(' ');
- buffer.append(suffix);
+ // Create the lambda accepting a `LogEvent` to invoke collected formatters
+ return logEvent -> {
+ final StringBuilder buffer = new StringBuilder();
+ buffer.append(' ');
+ for (PatternFormatter suffixFormatter : suffixFormatters) {
+ suffixFormatter.format(logEvent, buffer);
}
- if (i < limit) {
- buffer.append(options.getSeparator());
+ final boolean blankSuffix = buffer.length() == 1;
+ if (blankSuffix) {
+ return separator;
+ } else {
+ buffer.append(separator);
+ return buffer.toString();
}
- }
+ };
+
} else {
- throwable.printStackTrace(new PrintWriter(new StringBuilderWriter(buffer)));
+ return logEvent -> separator;
}
}
+ private static ThrowableStackTraceRenderer> createRenderer(final ThrowableFormatOptions options) {
+ return new ThrowableStackTraceRenderer<>(options.getIgnorePackages(), options.getLines());
+ }
+
/**
- * This converter obviously handles throwables.
+ * Returns the formatted suffix pattern.
*
- * @return true.
+ * @param logEvent the log event to use while formatting the suffix pattern
+ * @return the formatted suffix
+ * @deprecated Planned to be removed without a replacement
*/
- @Override
- public boolean handlesThrowable() {
- return true;
- }
-
- protected String getSuffix(final LogEvent event) {
- if (formatters.isEmpty()) {
- return Strings.EMPTY;
- }
- //noinspection ForLoopReplaceableByForEach
- final StringBuilder toAppendTo = new StringBuilder();
- for (int i = 0, size = formatters.size(); i < size; i++) {
- formatters.get(i).format(event, toAppendTo);
+ @Deprecated
+ protected String getSuffix(final LogEvent logEvent) {
+ requireNonNull(logEvent, "logEvent");
+ final String effectiveLineSeparator = effectiveLineSeparatorProvider.apply(logEvent);
+ if (options.getSeparator().equals(effectiveLineSeparator)) {
+ return "";
}
- return toAppendTo.toString();
- }
-
- public ThrowableFormatOptions getOptions() {
- return options;
+ return effectiveLineSeparator.substring(
+ // Skip whitespace prefix:
+ 1,
+ // Remove the separator:
+ effectiveLineSeparator.length() - options.getSeparator().length());
}
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRenderer.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRenderer.java
new file mode 100644
index 00000000000..62fce22e67b
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowablePropertyRenderer.java
@@ -0,0 +1,98 @@
+/*
+ * 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.pattern;
+
+import org.apache.logging.log4j.core.impl.ThrowableFormatOptions;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+@NullMarked
+enum ThrowablePropertyRenderer implements ThrowableRenderer {
+ MESSAGE(ThrowableFormatOptions.MESSAGE, (buffer, throwable, lineSeparator) -> {
+ final String message = throwable.getMessage();
+ buffer.append(message);
+ buffer.append(lineSeparator);
+ }),
+ LOCALIZED_MESSAGE(ThrowableFormatOptions.LOCALIZED_MESSAGE, (buffer, throwable, lineSeparator) -> {
+ final String localizedMessage = throwable.getLocalizedMessage();
+ buffer.append(localizedMessage);
+ buffer.append(lineSeparator);
+ }),
+ CLASS_NAME(ThrowableFormatOptions.CLASS_NAME, ((buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+ if (stackTraceElements != null && stackTraceElements.length > 0) {
+ final StackTraceElement throwingMethod = stackTraceElements[0];
+ final String className = throwingMethod.getClassName();
+ buffer.append(className);
+ buffer.append(lineSeparator);
+ }
+ })),
+ METHOD_NAME(ThrowableFormatOptions.METHOD_NAME, ((buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+ if (stackTraceElements != null && stackTraceElements.length > 0) {
+ final StackTraceElement throwingMethod = stackTraceElements[0];
+ final String methodName = throwingMethod.getMethodName();
+ buffer.append(methodName);
+ buffer.append(lineSeparator);
+ }
+ })),
+ LINE_NUMBER(ThrowableFormatOptions.LINE_NUMBER, ((buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+ if (stackTraceElements != null && stackTraceElements.length > 0) {
+ final StackTraceElement throwingMethod = stackTraceElements[0];
+ final int lineNumber = throwingMethod.getLineNumber();
+ buffer.append(lineNumber);
+ buffer.append(lineSeparator);
+ }
+ })),
+ FILE_NAME(ThrowableFormatOptions.FILE_NAME, ((buffer, throwable, lineSeparator) -> {
+ @Nullable final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+ if (stackTraceElements != null && stackTraceElements.length > 0) {
+ final StackTraceElement throwingMethod = stackTraceElements[0];
+ final String fileName = throwingMethod.getFileName();
+ buffer.append(fileName);
+ buffer.append(lineSeparator);
+ }
+ }));
+
+ private final String name;
+
+ private final ThrowableRenderer delegate;
+
+ ThrowablePropertyRenderer(final String name, final ThrowableRenderer delegate) {
+ this.name = name;
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void renderThrowable(final StringBuilder buffer, final Throwable throwable, final String lineSeparator) {
+ delegate.renderThrowable(buffer, throwable, lineSeparator);
+ }
+
+ @Nullable
+ static ThrowablePropertyRenderer fromOptions(@Nullable final String[] options) {
+ if (options != null && options.length > 0) {
+ final String name = options[0];
+ for (final ThrowablePropertyRenderer renderer : values()) {
+ if (renderer.name.equalsIgnoreCase(name)) {
+ return renderer;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyRendererTest.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableRenderer.java
similarity index 58%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyRendererTest.java
rename to log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableRenderer.java
index faa5ef9c869..0d65d8a4359 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/impl/ThrowableProxyRendererTest.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableRenderer.java
@@ -14,25 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.logging.log4j.core.impl;
-
-import java.util.ArrayList;
-import org.apache.logging.log4j.core.pattern.PlainTextRenderer;
-import org.junit.jupiter.api.Test;
+package org.apache.logging.log4j.core.pattern;
/**
- * Tests ThrowableProxyRenderer.
+ * Contract for rendering {@link Throwable}s in {@link ThrowablePatternConverter} et al.
*/
-public class ThrowableProxyRendererTest {
+interface ThrowableRenderer {
- @Test
- public void test_formatExtendedStackTraceTo() {
- ThrowableProxyRenderer.formatExtendedStackTraceTo(
- new ThrowableProxy(),
- new StringBuilder(),
- new ArrayList<>(),
- new PlainTextRenderer(),
- "",
- System.lineSeparator());
- }
+ void renderThrowable(final StringBuilder buffer, final Throwable throwable, final String lineSeparator);
}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
new file mode 100644
index 00000000000..b16e9b98362
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/ThrowableStackTraceRenderer.java
@@ -0,0 +1,333 @@
+/*
+ * 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.pattern;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * {@link ThrowableRenderer} implementation for rendering stack traces.
+ *
+ * @param the context type
+ */
+@NullMarked
+class ThrowableStackTraceRenderer implements ThrowableRenderer {
+
+ private static final RuntimeException MAX_LINE_COUNT_EXCEEDED = new RuntimeException("max-line-count-exceeded");
+
+ private static final String CAUSED_BY_CAPTION = "Caused by: ";
+
+ static final String SUPPRESSED_CAPTION = "Suppressed: ";
+
+ final List ignoredPackageNames;
+
+ final int maxLineCount;
+
+ ThrowableStackTraceRenderer(final List ignoredPackageNames, final int maxLineCount) {
+ this.ignoredPackageNames = ignoredPackageNames;
+ this.maxLineCount = maxLineCount;
+ }
+
+ @Override
+ public final void renderThrowable(
+ final StringBuilder buffer, final Throwable throwable, final String lineSeparator) {
+ if (maxLineCount > 0) {
+ try {
+ C context = createContext(throwable);
+ renderThrowable(buffer, throwable, context, new HashSet<>(), lineSeparator);
+ } catch (final Exception error) {
+ if (error != MAX_LINE_COUNT_EXCEEDED) {
+ throw error;
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ C createContext(final Throwable throwable) {
+ final Map metadataByThrowable = Context.Metadata.ofThrowable(throwable);
+ return (C) new Context(0, metadataByThrowable);
+ }
+
+ void renderThrowable(
+ final StringBuilder buffer,
+ final Throwable throwable,
+ final C context,
+ final Set visitedThrowables,
+ final String lineSeparator) {
+ renderThrowable(buffer, throwable, context, visitedThrowables, "", lineSeparator, "");
+ }
+
+ private void renderThrowable(
+ final StringBuilder buffer,
+ final Throwable throwable,
+ final C context,
+ final Set visitedThrowables,
+ final String prefix,
+ final String lineSeparator,
+ final String caption) {
+ acquireLineCapacity(context);
+ final boolean circular = !visitedThrowables.add(throwable);
+ buffer.append(prefix);
+ buffer.append(caption);
+ if (circular) {
+ buffer.append("[CIRCULAR REFERENCE: ");
+ renderThrowableMessage(buffer, throwable);
+ buffer.append(']');
+ buffer.append(lineSeparator);
+ } else {
+ renderThrowableMessage(buffer, throwable);
+ buffer.append(lineSeparator);
+ renderStackTraceElements(buffer, throwable, context, prefix, lineSeparator);
+ renderSuppressed(
+ buffer, throwable.getSuppressed(), context, visitedThrowables, prefix + '\t', lineSeparator);
+ renderCause(buffer, throwable.getCause(), context, visitedThrowables, prefix, lineSeparator);
+ }
+ }
+
+ void acquireLineCapacity(final C context) {
+ if (context.lineCount < maxLineCount) {
+ context.lineCount++;
+ } else {
+ throw MAX_LINE_COUNT_EXCEEDED;
+ }
+ }
+
+ void renderSuppressed(
+ final StringBuilder buffer,
+ final Throwable[] suppressedThrowables,
+ final C context,
+ final Set visitedThrowables,
+ final String prefix,
+ final String lineSeparator) {
+ for (final Throwable suppressedThrowable : suppressedThrowables) {
+ renderThrowable(
+ buffer, suppressedThrowable, context, visitedThrowables, prefix, lineSeparator, SUPPRESSED_CAPTION);
+ }
+ }
+
+ private void renderCause(
+ final StringBuilder buffer,
+ @Nullable final Throwable cause,
+ final C context,
+ final Set visitedThrowables,
+ final String prefix,
+ final String lineSeparator) {
+ if (cause != null) {
+ renderThrowable(buffer, cause, context, visitedThrowables, prefix, lineSeparator, CAUSED_BY_CAPTION);
+ }
+ }
+
+ static void renderThrowableMessage(final StringBuilder buffer, final Throwable throwable) {
+ final String message = throwable.getLocalizedMessage();
+ buffer.append(throwable.getClass().getName());
+ if (message != null) {
+ buffer.append(": ");
+ buffer.append(message);
+ }
+ }
+
+ final void renderStackTraceElements(
+ final StringBuilder buffer,
+ final Throwable throwable,
+ final C context,
+ final String prefix,
+ final String lineSeparator) {
+ context.ignoredStackTraceElementCount = 0;
+ final Context.Metadata metadata = context.metadataByThrowable.get(throwable);
+ final StackTraceElement[] stackTraceElements = throwable.getStackTrace();
+ for (int i = 0; i < metadata.stackLength; i++) {
+ renderStackTraceElement(buffer, stackTraceElements[i], context, prefix, lineSeparator);
+ }
+ if (context.ignoredStackTraceElementCount > 0) {
+ renderSuppressedCount(buffer, context, prefix, lineSeparator);
+ }
+ if (metadata.commonElementCount != 0) {
+ acquireLineCapacity(context);
+ buffer.append(prefix);
+ buffer.append("\t... ");
+ buffer.append(metadata.commonElementCount);
+ buffer.append(" more");
+ buffer.append(lineSeparator);
+ }
+ }
+
+ void renderStackTraceElement(
+ final StringBuilder buffer,
+ final StackTraceElement stackTraceElement,
+ final C context,
+ final String prefix,
+ final String lineSeparator) {
+
+ // Short-circuit on ignored stack trace elements
+ final boolean stackTraceElementIgnored = isStackTraceElementIgnored(stackTraceElement);
+ if (stackTraceElementIgnored) {
+ context.ignoredStackTraceElementCount += 1;
+ return;
+ }
+
+ // Render the suppressed stack trace element count
+ if (context.ignoredStackTraceElementCount > 0) {
+ renderSuppressedCount(buffer, context, prefix, lineSeparator);
+ context.ignoredStackTraceElementCount = 0;
+ }
+
+ // Render the stack trace element
+ acquireLineCapacity(context);
+ buffer.append(prefix);
+ buffer.append("\tat ");
+ buffer.append(stackTraceElement);
+ buffer.append(lineSeparator);
+ }
+
+ boolean isStackTraceElementIgnored(final StackTraceElement element) {
+ if (ignoredPackageNames != null) {
+ final String className = element.getClassName();
+ for (final String ignoredPackageName : ignoredPackageNames) {
+ if (className.startsWith(ignoredPackageName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ void renderSuppressedCount(
+ final StringBuilder buffer, final C context, final String prefix, final String lineSeparator) {
+ acquireLineCapacity(context);
+ buffer.append(prefix);
+ if (context.ignoredStackTraceElementCount == 1) {
+ buffer.append("\t...");
+ } else {
+ buffer.append("\t... suppressed ");
+ buffer.append(context.ignoredStackTraceElementCount);
+ buffer.append(" lines");
+ }
+ buffer.append(lineSeparator);
+ }
+
+ static class Context {
+
+ /**
+ * Number of lines consumed from the {@link Throwable} causal chain so far.
+ */
+ int lineCount = 0;
+
+ /**
+ * Number of stack trace elements ignored.
+ *
+ * This value will be reset per {@link Throwable} in the causal chain.
+ *
+ */
+ int ignoredStackTraceElementCount;
+
+ /**
+ * {@link Metadata} per {@link Throwable} in the causal chain
+ */
+ final Map metadataByThrowable;
+
+ /**
+ * The canonical constructor.
+ */
+ Context(final int ignoredStackTraceElementCount, final Map metadataByThrowable) {
+ this.ignoredStackTraceElementCount = ignoredStackTraceElementCount;
+ this.metadataByThrowable = metadataByThrowable;
+ }
+
+ /**
+ * Invariants associated with a {@link Throwable}
+ */
+ static final class Metadata {
+
+ /**
+ * Number of stack trace elements shared with the parent {@link Throwable}'s stack
+ */
+ final int commonElementCount;
+
+ /**
+ * Number of stack trace elements exclusive to this {@link Throwable}, i.e., not in common with the parent {@link Throwable}'s stack
+ */
+ final int stackLength;
+
+ private Metadata(final int commonElementCount, final int stackLength) {
+ this.commonElementCount = commonElementCount;
+ this.stackLength = stackLength;
+ }
+
+ static Map ofThrowable(final Throwable throwable) {
+ final Map metadataByThrowable = new HashMap<>();
+ populateMetadata(metadataByThrowable, new HashSet<>(), null, throwable);
+ return metadataByThrowable;
+ }
+
+ private static void populateMetadata(
+ final Map metadataByThrowable,
+ final Set visitedThrowables,
+ @Nullable final Throwable parentThrowable,
+ final Throwable throwable) {
+
+ // Populate metadata of the current throwable
+ @Nullable
+ final StackTraceElement[] rootTrace = parentThrowable == null ? null : parentThrowable.getStackTrace();
+ final Metadata metadata = populateMetadata(rootTrace, throwable.getStackTrace());
+ metadataByThrowable.put(throwable, metadata);
+
+ // Populate metadata of suppressed exceptions
+ for (final Throwable suppressed : throwable.getSuppressed()) {
+ if (!visitedThrowables.contains(suppressed)) {
+ visitedThrowables.add(suppressed);
+ populateMetadata(metadataByThrowable, visitedThrowables, throwable, suppressed);
+ }
+ }
+
+ // Populate metadata of the causal chain
+ @Nullable final Throwable cause = throwable.getCause();
+ if (cause != null && !visitedThrowables.contains(cause)) {
+ visitedThrowables.add(cause);
+ populateMetadata(metadataByThrowable, visitedThrowables, throwable, cause);
+ }
+ }
+
+ private static Metadata populateMetadata(
+ @Nullable final StackTraceElement[] parentTrace, final StackTraceElement[] currentTrace) {
+ int commonElementCount;
+ int stackLength;
+ if (parentTrace != null) {
+ int parentIndex = parentTrace.length - 1;
+ int currentIndex = currentTrace.length - 1;
+ while (parentIndex >= 0
+ && currentIndex >= 0
+ && parentTrace[parentIndex].equals(currentTrace[currentIndex])) {
+ --parentIndex;
+ --currentIndex;
+ }
+ commonElementCount = currentTrace.length - 1 - currentIndex;
+ stackLength = currentIndex + 1;
+ } else {
+ commonElementCount = 0;
+ stackLength = currentTrace.length;
+ }
+ return new Metadata(commonElementCount, stackLength);
+ }
+ }
+ }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
index 5818a773fe9..5c047848b57 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/package-info.java
@@ -18,7 +18,7 @@
* Provides classes implementing format specifiers in conversion patterns.
*/
@Export
-@Version("2.24.0")
+@Version("2.25.0")
package org.apache.logging.log4j.core.pattern;
import org.osgi.annotation.bundle.Export;
diff --git a/log4j-perf-test/src/main/java/org/apache/logging/log4j/ThrowVsReturnBenchmark.java b/log4j-perf-test/src/main/java/org/apache/logging/log4j/ThrowVsReturnBenchmark.java
new file mode 100644
index 00000000000..1c1d59b1ba7
--- /dev/null
+++ b/log4j-perf-test/src/main/java/org/apache/logging/log4j/ThrowVsReturnBenchmark.java
@@ -0,0 +1,78 @@
+/*
+ * 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.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+@State(Scope.Benchmark)
+public class ThrowVsReturnBenchmark {
+
+ private static final int SALT = ThreadLocalRandom.current().nextInt();
+
+ private static final RuntimeException EXCEPTION = new RuntimeException();
+
+ private static final int MAX_DEPTH = 5;
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public int withReturn(final Blackhole blackhole) {
+ return withReturn(blackhole, 0);
+ }
+
+ private static int withReturn(final Blackhole blackhole, final int depth) {
+ doWork(blackhole);
+ return depth < MAX_DEPTH ? withReturn(blackhole, depth + 1) : blackhole.i1;
+ }
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public int withThrow(final Blackhole blackhole) {
+ try {
+ withThrow(blackhole, 0);
+ } catch (final Exception error) {
+ if (error == EXCEPTION) {
+ return blackhole.i1;
+ }
+ }
+ throw new IllegalStateException();
+ }
+
+ private static void withThrow(final Blackhole blackhole, final int depth) {
+ doWork(blackhole);
+ if (depth < MAX_DEPTH) {
+ withThrow(blackhole, depth + 1);
+ } else {
+ throw EXCEPTION;
+ }
+ }
+
+ private static void doWork(final Blackhole blackhole) {
+ for (int i = 0; i < 1_000; i++) {
+ blackhole.consume((i << 10) + SALT);
+ }
+ }
+}
diff --git a/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/CatchingTagTest.java b/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/CatchingTagTest.java
index 7bf7c795cff..a7eeb30f033 100644
--- a/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/CatchingTagTest.java
+++ b/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/CatchingTagTest.java
@@ -24,6 +24,7 @@
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.test.appender.ListAppender;
import org.apache.logging.log4j.core.test.junit.LoggerContextRule;
+import org.apache.logging.log4j.util.Strings;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
@@ -79,7 +80,10 @@ private void verify(final String expected) {
final List events = listApp.getMessages();
try {
assertEquals("Incorrect number of messages.", 1, events.size());
- assertEquals("Incorrect message.", "o.a.l.l.t.CatchingTagTest " + expected, events.get(0));
+ assertEquals(
+ "Incorrect message.",
+ "o.a.l.l.t.CatchingTagTest " + expected + Strings.LINE_SEPARATOR,
+ events.get(0));
} finally {
listApp.clear();
}
diff --git a/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/LoggingMessageTagSupportTest.java b/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/LoggingMessageTagSupportTest.java
index 9826603000f..d64bb1711cc 100644
--- a/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/LoggingMessageTagSupportTest.java
+++ b/log4j-taglib/src/test/java/org/apache/logging/log4j/taglib/LoggingMessageTagSupportTest.java
@@ -29,6 +29,7 @@
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.core.test.appender.ListAppender;
import org.apache.logging.log4j.core.test.junit.LoggerContextRule;
+import org.apache.logging.log4j.util.Strings;
import org.junit.ClassRule;
import org.junit.Test;
import org.springframework.mock.web.MockBodyContent;
@@ -136,7 +137,8 @@ public void testDoEndTagStringMessageNoMarkerException() throws Exception {
assertEquals("The return value is not correct.", Tag.EVAL_PAGE, this.tag.doEndTag());
verify(
- "Another message for testDoEndTagStringMessageNoMarkerException ERROR M- E java.lang.Exception: This is a test");
+ "Another message for testDoEndTagStringMessageNoMarkerException ERROR M- E java.lang.Exception: This is a test"
+ + Strings.LINE_SEPARATOR);
}
@Test
@@ -149,7 +151,8 @@ public void testDoEndTagStringMessageMarkerException() throws Exception {
assertEquals("The return value is not correct.", Tag.EVAL_PAGE, this.tag.doEndTag());
verify(
- "Final message for testDoEndTagStringMessageMarkerException TRACE M-F02 E java.lang.RuntimeException: This is another test");
+ "Final message for testDoEndTagStringMessageMarkerException TRACE M-F02 E java.lang.RuntimeException: This is another test"
+ + Strings.LINE_SEPARATOR);
}
@Test
@@ -175,7 +178,8 @@ public void testDoEndTagStringWithParametersMarkerAndException() throws Exceptio
this.tag.setMessage("Final message with [{}] parameter of [{}]");
assertEquals("The return value is not correct.", Tag.EVAL_PAGE, this.tag.doEndTag());
- verify("Final message with [Z] parameter of [SECONDS] DEBUG M-N03 E java.lang.Error: This is the last test");
+ verify("Final message with [Z] parameter of [SECONDS] DEBUG M-N03 E java.lang.Error: This is the last test"
+ + Strings.LINE_SEPARATOR);
}
@Test
@@ -210,7 +214,8 @@ public void testDoEndTagMessageNoMarkerException() throws Exception {
logger.getMessageFactory().newMessage("Third message for testDoEndTagMessageNoMarkerException"));
assertEquals("The return value is not correct.", Tag.EVAL_PAGE, this.tag.doEndTag());
- verify("Third message for testDoEndTagMessageNoMarkerException TRACE M- E java.lang.Exception: This is a test");
+ verify("Third message for testDoEndTagMessageNoMarkerException TRACE M- E java.lang.Exception: This is a test"
+ + Strings.LINE_SEPARATOR);
}
@Test
@@ -224,7 +229,7 @@ public void testDoEndTagMessageMarkerException() throws Exception {
assertEquals("The return value is not correct.", Tag.EVAL_PAGE, this.tag.doEndTag());
verify("Final message for testDoEndTagMessageMarkerException ERROR M-F02 E java.lang.RuntimeException: "
- + "This is another test");
+ + "This is another test" + Strings.LINE_SEPARATOR);
}
@Test
@@ -256,7 +261,8 @@ public void testDoEndTagObjectNoMarkerException() throws Exception {
this.tag.setMessage(new MyMessage("Third message for testDoEndTagObjectNoMarkerException"));
assertEquals("The return value is not correct.", Tag.EVAL_PAGE, this.tag.doEndTag());
- verify("Third message for testDoEndTagObjectNoMarkerException TRACE M- E java.lang.Exception: This is a test");
+ verify("Third message for testDoEndTagObjectNoMarkerException TRACE M- E java.lang.Exception: This is a test"
+ + Strings.LINE_SEPARATOR);
}
@Test
@@ -269,7 +275,7 @@ public void testDoEndTagObjectMarkerException() throws Exception {
assertEquals("The return value is not correct.", Tag.EVAL_PAGE, this.tag.doEndTag());
verify("Final message for testDoEndTagObjectMarkerException ERROR M-F02 E java.lang.RuntimeException: "
- + "This is another test");
+ + "This is another test" + Strings.LINE_SEPARATOR);
}
private void verify(final String expected) {
diff --git a/src/changelog/.2.x.x/2691_change_PatternLayout_exception_rendering.xml b/src/changelog/.2.x.x/2691_change_PatternLayout_exception_rendering.xml
new file mode 100644
index 00000000000..6d5d90c0eea
--- /dev/null
+++ b/src/changelog/.2.x.x/2691_change_PatternLayout_exception_rendering.xml
@@ -0,0 +1,8 @@
+
+
+
+ Consolidate exception rendering logic and improve circular reference support in Pattern Layout
+
diff --git a/src/changelog/.2.x.x/2691_deprecate_ThrowableProxy.xml b/src/changelog/.2.x.x/2691_deprecate_ThrowableProxy.xml
new file mode 100644
index 00000000000..8110ff25e3f
--- /dev/null
+++ b/src/changelog/.2.x.x/2691_deprecate_ThrowableProxy.xml
@@ -0,0 +1,8 @@
+
+
+
+ Deprecate `ThrowableProxy` and all its usages
+
diff --git a/src/changelog/.2.x.x/2691_fix_PatternLayout_exception_rendering.xml b/src/changelog/.2.x.x/2691_fix_PatternLayout_exception_rendering.xml
new file mode 100644
index 00000000000..a3b27c26c35
--- /dev/null
+++ b/src/changelog/.2.x.x/2691_fix_PatternLayout_exception_rendering.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+ Fix certain exception rendering issues in Pattern Layout
+
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index dcf5b29b530..4734a3e39ca 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -624,7 +624,7 @@ ex|exception|throwable
{separator(separator)}
----
-By default this will output the full trace as one would normally find with a call to `Throwable#printStackTrace()`.
+By default this will output the full stack trace as one would normally find with a call to `Throwable#printStackTrace()`.
You can follow the throwable conversion word with an option in the form `%throwable\{option}`.
@@ -632,11 +632,11 @@ You can follow the throwable conversion word with an option in the form `%throwa
`%throwable{short.className}` outputs the name of the class where the exception occurred.
-`%throwable{short.methodName}` outputs the method name where the exception occurred.
+`%throwable{short.methodName}` outputs the name of the method where the exception occurred.
-`%throwable{short.fileName}` outputs the name of the class where the exception occurred.
+`%throwable{short.fileName}` outputs the name of the file containing the class where the exception occurred.
-`%throwable{short.lineNumber}` outputs the line number where the exception occurred.
+`%throwable{short.lineNumber}` outputs the line number of the file containing the class where the exception occurred.
`%throwable{short.message}` outputs the message.
@@ -646,12 +646,28 @@ You can follow the throwable conversion word with an option in the form `%throwa
Specifying `%throwable\{none}` or `%throwable\{0}` suppresses output of the exception.
-Use `{filters(packages)}` where _packages_ is a list of package names to suppress matching stack frames from stack traces.
+Use `{filters(packages)}`, where `packages` is a list of package names, to suppress matching stack frames from stack traces.
-Use `{suffix(pattern)}` to add the output of _pattern_ at the end of each stack frame.
+You can change the used line separator in multiple ways:
-Use a `{separator(...)}` as the end-of-line string, e.g., `separator(\|)`.
-The default value is the `line.separator` system property, which is operating system dependent.
+* Use `{separator(separator)}` to set the separator string literal.
+It defaults to `System.lineSeparator()`.
+The contents of `separator` will be rendered verbatim without being subject to any processing.
+
+* `{suffix(pattern)}` is identical to `{separator(separator)}` with the exception that the provided `pattern` will be processed as a xref:manual/pattern-layout.adoc[] conversion pattern before being rendered.
+Exception-rendering directives in the `pattern` (`%ex`, `%rEx`, etc.) will be discarded.
+
+`{separator(...)}` and `{suffix(pattern)}` get concatenated to produce _the effective line separator_ as follows:
+
+[source,java]
+----
+String effectiveLineSeparator(String separator, String suffix, LogEvent event) {
+ String formattedSuffix = format(suffix, event);
+ return isNotBlank(formattedSuffix)
+ ? (' ' + formattedSuffix + lineSeparator)
+ : lineSeparator;
+}
+----
[WARNING]
====
@@ -661,7 +677,7 @@ Exception converter is not garbage-free.
[#converter-exception-extended]
==== Exception (Extended)
-The same as <>, but also includes class packaging information
+The same as <>, but also includes class packaging information
.link:../javadoc/log4j-core/org/apache/logging/log4j/core/pattern/ThrowablePatternConverter.html[`ThrowablePatternConverter`] specifier grammar
[source,text]
@@ -1180,30 +1196,22 @@ The same as <>, but the stack tra
[source,text]
----
rEx|rException|rThrowable
- {
- ["none" | "short" | "full" | depth]
- [,filters(package,package,...)]
- [,separator(separator)]
- }
- {ansi(
- Key=Value,Value,...
- Key=Value,Value,...
- ...)
+ { "none"
+ | "full"
+ | depth
+ | "short"
+ | "short.className"
+ | "short.fileName"
+ | "short.lineNumber"
+ | "short.methodName"
+ | "short.message"
+ | "short.localizedMessage"
}
+ {filters(package,package,...)}
{suffix(pattern)}
+ {separator(separator)}
----
-The throwable conversion specifier can be followed by an option in the form `%rEx\{short}`, which will only output the first line of the `Throwable`, or `%rEx\{n}`, where the first `n` lines of the stack trace will be printed.
-
-Specifying `%rEx\{none}` or `%rEx\{0}` will suppress printing of the exception.
-
-Use `filters(packages)`, where `packages` is a list of package names to suppress matching stack frames from stack traces.
-
-Use a `separator` string to separate the lines of a stack trace, e.g., `separator(|)`.
-The default value is the `line.separator` system property, which is platform dependent.
-
-Use `rEx{suffix(pattern)}` to add the output of `pattern` to the output only when there is a `Throwable` to print.
-
[#converter-seq]
==== Sequence number