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 + * + *

+ * 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