You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2026/09/17 07:48:53 UTC

(logging-log4j2) branch 2.x updated: Fix Pattern Layout's stack trace rendering for non-idempotent `Throwable::getCause` (#4303)

This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/2.x by this push:
     new 9bcbf7e160 Fix Pattern Layout's stack trace rendering for non-idempotent `Throwable::getCause` (#4303)
9bcbf7e160 is described below

commit 9bcbf7e1603e9e1b50a0fcc489f065f001d7c1c7
Author: Usman Ejaz <12...@users.noreply.github.com>
AuthorDate: Thu Sep 17 12:48:47 2026 +0500

    Fix Pattern Layout's stack trace rendering for non-idempotent `Throwable::getCause` (#4303)
    
    Co-authored-by: Volkan Yazıcı <vo...@yazi.ci>
---
 .../src/test/java/foo/TestFriendlyException.java   | 59 +++++++++++++++++++---
 .../ExtendedThrowablePatternConverterTest.java     | 12 +++++
 .../pattern/RootThrowablePatternConverterTest.java | 12 +++++
 .../pattern/ThrowablePatternConverterTest.java     | 12 +++++
 .../ThrowableInvertedStackTraceRenderer.java       |  8 +--
 .../core/pattern/ThrowableStackTraceRenderer.java  | 25 ++++++---
 ...eStackTraceRenderer_non_idempotent_getCause.xml | 13 +++++
 7 files changed, 121 insertions(+), 20 deletions(-)

diff --git a/log4j-core-test/src/test/java/foo/TestFriendlyException.java b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
index 5d451c6a54..f0201e04f6 100644
--- a/log4j-core-test/src/test/java/foo/TestFriendlyException.java
+++ b/log4j-core-test/src/test/java/foo/TestFriendlyException.java
@@ -36,6 +36,7 @@ import org.apache.logging.log4j.util.Constants;
  * <li>Non-Log4j<sup>1</sup> and fixed<sup>2</sup> (to {@code bar}) package origin</li>
  * <li>Sufficient causal chain depth</li>
  * <li>Circular causal chain</li>
+ * <li>Non-idempotent causal chain</li>
  * <li>Suppressed exceptions</li>
  * <li>Clutter-free stack trace (i.e., elements from JUnit, JDK, etc.)</li>
  * <li>Stack trace elements from named modules<sup>3</sup></li>
@@ -63,6 +64,8 @@ public final class TestFriendlyException extends RuntimeException {
         assertThat(TestFriendlyException.class.getPackage().getName()).doesNotStartWith("org.apache");
     }
 
+    private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];
+
     public static final StackTraceElement ORG_APACHE_REPLACEMENT_STACK_TRACE_ELEMENT =
             new StackTraceElement("bar.OrgApacheReplacement", "someMethod", "OrgApacheReplacement.java", 0);
 
@@ -95,8 +98,15 @@ public final class TestFriendlyException extends RuntimeException {
         "java.lang", "jdk.internal", "org.junit", "sun.reflect"
     };
 
-    public static final TestFriendlyException INSTANCE =
-            create("r", 0, 2, new boolean[] {false}, new boolean[] {true}, new boolean[] {true}, new int[] {5});
+    public static final TestFriendlyException INSTANCE = create(
+            "r",
+            0,
+            2,
+            new boolean[] {false},
+            new boolean[] {true},
+            new boolean[] {true},
+            new boolean[] {true},
+            new int[] {5});
 
     static {
         ensureIdentityMalfunctionAtDifferentDepths();
@@ -110,20 +120,22 @@ public final class TestFriendlyException extends RuntimeException {
     private static void ensureIdentityMalfunctionAtDifferentDepths() {
         final Set<Throwable> visitedExceptions = Collections.newSetFromMap(new IdentityHashMap<>());
         final Set<Integer> identityMalfunctioningExceptionStackTraceDepths = new HashSet<>();
-        final Queue<TestFriendlyException> exceptions = new LinkedList<>();
+        final Queue<Throwable> exceptions = new LinkedList<>();
         exceptions.add(INSTANCE);
         while (!exceptions.isEmpty()) {
-            final TestFriendlyException exception = exceptions.remove();
-            if (!visitedExceptions.add(exception) || !exception.identityMalfunctioning) {
+            final Throwable exception = exceptions.remove();
+            if (!visitedExceptions.add(exception)
+                    || (exception instanceof TestFriendlyException
+                            && !((TestFriendlyException) exception).identityMalfunctioning)) {
                 continue;
             }
             identityMalfunctioningExceptionStackTraceDepths.add(exception.getStackTrace().length);
-            final TestFriendlyException cause = (TestFriendlyException) exception.getCause();
+            final Throwable cause = exception.getCause();
             if (cause != null) {
                 exceptions.add(cause);
             }
             for (final Throwable suppressed : exception.getSuppressed()) {
-                exceptions.add((TestFriendlyException) suppressed);
+                exceptions.add(suppressed);
             }
         }
         assertThat(identityMalfunctioningExceptionStackTraceDepths)
@@ -136,40 +148,62 @@ public final class TestFriendlyException extends RuntimeException {
             final int depth,
             final int maxDepth,
             final boolean[] circular,
+            final boolean[] nonIdempotentCauseAllowed,
             final boolean[] namedModuleAllowed,
             final boolean[] nonExistentClassAllowed,
             final int[] maxIdentityMalfunctionCount) {
         final TestFriendlyException error = new TestFriendlyException(
                 name, namedModuleAllowed, nonExistentClassAllowed, maxIdentityMalfunctionCount);
         if (depth < maxDepth) {
+
+            // Set the cause
             final TestFriendlyException cause = create(
                     name + "_c",
                     depth + 1,
                     maxDepth,
                     circular,
+                    nonIdempotentCauseAllowed,
                     namedModuleAllowed,
                     nonExistentClassAllowed,
                     maxIdentityMalfunctionCount);
             error.initCause(cause);
+
+            // Add a suppressed exception
             final TestFriendlyException suppressed = create(
                     name + "_s",
                     depth + 1,
                     maxDepth,
                     circular,
+                    nonIdempotentCauseAllowed,
                     namedModuleAllowed,
                     nonExistentClassAllowed,
                     maxIdentityMalfunctionCount);
             error.addSuppressed(suppressed);
+
+            // Make cause and the suppressed exceptions circular, if allowed
             final boolean circularAllowed = depth + 1 == maxDepth && !circular[0];
             if (circularAllowed) {
                 cause.initCause(error);
                 suppressed.initCause(error);
                 circular[0] = true;
             }
+
+            // Add a suppressed exception with a non-idempotent cause, if allowed.
+            // We add this odd exception sort deliberately as a suppressed exception.
+            // Having it in the causal chain breaks circular references, if present.
+            if (nonIdempotentCauseAllowed[0]) {
+                final TestFriendlyException exception = new TestFriendlyException(
+                        name + "_S", namedModuleAllowed, nonExistentClassAllowed, maxIdentityMalfunctionCount);
+                exception.nonIdempotentCause = true;
+                error.addSuppressed(exception);
+                nonIdempotentCauseAllowed[0] = false;
+            }
         }
         return error;
     }
 
+    private boolean nonIdempotentCause = false;
+
     private final boolean identityMalfunctioning;
 
     private TestFriendlyException(
@@ -251,6 +285,17 @@ public final class TestFriendlyException extends RuntimeException {
         return Stream.of(NON_EXISTENT_CLASS_STACK_TRACE_ELEMENT);
     }
 
+    @Override
+    public synchronized Throwable getCause() {
+        return nonIdempotentCause ? createEmptyThrowable() : super.getCause();
+    }
+
+    private static Throwable createEmptyThrowable() {
+        final Throwable throwable = new Throwable("Empty test throwable");
+        throwable.setStackTrace(EMPTY_STACK_TRACE);
+        return throwable;
+    }
+
     @Override
     public String getLocalizedMessage() {
         return getMessage() + " [localized]";
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 7f2f16132f..bb74033a0d 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
@@ -117,6 +117,10 @@ class ExtendedThrowablePatternConverterTest {
             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
             "		... 3 more",
             "	Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+            "	Suppressed: foo.TestFriendlyException: r_c_S [localized]",
+            "		at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+            "		... 3 more",
+            "	Caused by: java.lang.Throwable: Empty test throwable",
             "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/:?]",
@@ -177,6 +181,10 @@ class ExtendedThrowablePatternConverterTest {
                             "		... suppressed 2 lines",
                             "		... 3 more",
                             "	Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+                            "	Suppressed: foo.TestFriendlyException: r_c_S [localized]",
+                            "		...",
+                            "		... 3 more",
+                            "	Caused by: java.lang.Throwable: Empty test throwable",
                             "Caused by: foo.TestFriendlyException: r_c_c [localized]",
                             "	... suppressed 2 lines",
                             "	... 3 more",
@@ -220,6 +228,10 @@ class ExtendedThrowablePatternConverterTest {
                             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
                             "		... 3 more",
                             "	Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+                            "	Suppressed: foo.TestFriendlyException: r_c_S [localized]",
+                            "		at foo.TestFriendlyException.create(TestFriendlyException.java:0) ~[test-classes/:?]",
+                            "		... 3 more",
+                            "	Caused by: java.lang.Throwable: Empty test throwable",
                             "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/:?]",
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 992b3fc3a3..2a92b033bf 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
@@ -64,6 +64,10 @@ class RootThrowablePatternConverterTest {
             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
             "		... 3 more",
+            "java.lang.Throwable: Empty test throwable",
+            "	Wrapped by: foo.TestFriendlyException: r_c_S [localized]",
+            "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+            "		... 3 more",
             "Wrapped by: foo.TestFriendlyException: r [localized]",
             "	at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
             "\tat " + TestFriendlyException.NON_EXISTENT_CLASS_STACK_TRACE_ELEMENT,
@@ -137,6 +141,10 @@ class RootThrowablePatternConverterTest {
                             "	Wrapped by: foo.TestFriendlyException: r_c_s [localized]",
                             "		... suppressed 2 lines",
                             "		... 3 more",
+                            "java.lang.Throwable: Empty test throwable",
+                            "	Wrapped by: foo.TestFriendlyException: r_c_S [localized]",
+                            "		...",
+                            "		... 3 more",
                             "Wrapped by: foo.TestFriendlyException: r [localized]",
                             "	at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
                             "\tat " + TestFriendlyException.NON_EXISTENT_CLASS_STACK_TRACE_ELEMENT,
@@ -177,6 +185,10 @@ class RootThrowablePatternConverterTest {
                             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
                             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
                             "		... 3 more",
+                            "java.lang.Throwable: Empty test throwable",
+                            "	Wrapped by: foo.TestFriendlyException: r_c_S [localized]",
+                            "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+                            "		... 3 more",
                             "Wrapped by: foo.TestFriendlyException: r [localized]",
                             "	at " + TestFriendlyException.NAMED_MODULE_STACK_TRACE_ELEMENT,
                             "\tat " + TestFriendlyException.NON_EXISTENT_CLASS_STACK_TRACE_ELEMENT,
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 17a9812b2f..c5563a4d82 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
@@ -329,6 +329,10 @@ public class ThrowablePatternConverterTest {
                             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
                             "		... 3 more",
                             "	Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+                            "	Suppressed: foo.TestFriendlyException: r_c_S [localized]",
+                            "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+                            "		... 3 more",
+                            "	Caused by: java.lang.Throwable: Empty test throwable",
                             "Caused by: foo.TestFriendlyException: r_c_c [localized]",
                             "	at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
                             "	at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
@@ -367,6 +371,10 @@ public class ThrowablePatternConverterTest {
                             "		... suppressed 2 lines",
                             "		... 3 more",
                             "	Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+                            "	Suppressed: foo.TestFriendlyException: r_c_S [localized]",
+                            "		...",
+                            "		... 3 more",
+                            "	Caused by: java.lang.Throwable: Empty test throwable",
                             "Caused by: foo.TestFriendlyException: r_c_c [localized]",
                             "	... suppressed 2 lines",
                             "	... 3 more",
@@ -410,6 +418,10 @@ public class ThrowablePatternConverterTest {
                             "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
                             "		... 3 more",
                             "	Caused by: [CIRCULAR REFERENCE: foo.TestFriendlyException: r_c [localized]]",
+                            "	Suppressed: foo.TestFriendlyException: r_c_S [localized]",
+                            "		at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
+                            "		... 3 more",
+                            "	Caused by: java.lang.Throwable: Empty test throwable",
                             "Caused by: foo.TestFriendlyException: r_c_c [localized]",
                             "	at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
                             "	at foo.TestFriendlyException.create(TestFriendlyException.java:0)",
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
index ca8a28ea1c..9cc05c9ce3 100644
--- 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
@@ -62,13 +62,7 @@ final class ThrowableInvertedStackTraceRenderer
         } else {
             final Context.Metadata metadata = context.metadataByThrowable.get(throwable);
             lineCapacityAcquired = renderCause(
-                    buffer,
-                    throwable.getCause(),
-                    context,
-                    visitedThrowables,
-                    prefix,
-                    lineSeparator,
-                    lineCapacityAcquired);
+                    buffer, metadata.cause, context, visitedThrowables, prefix, lineSeparator, lineCapacityAcquired);
             if (!lineCapacityAcquired) {
                 acquireLineCapacity(context);
             }
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
index cafb2a2a93..519708b8cb 100644
--- 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
@@ -103,7 +103,7 @@ class ThrowableStackTraceRenderer<C extends ThrowableStackTraceRenderer.Context>
             buffer.append(lineSeparator);
             renderStackTraceElements(buffer, context, metadata, prefix, lineSeparator);
             renderSuppressed(buffer, metadata.suppressed, context, visitedThrowables, prefix + '\t', lineSeparator);
-            renderCause(buffer, throwable.getCause(), context, visitedThrowables, prefix, lineSeparator);
+            renderCause(buffer, metadata.cause, context, visitedThrowables, prefix, lineSeparator);
         }
     }
 
@@ -283,15 +283,27 @@ class ThrowableStackTraceRenderer<C extends ThrowableStackTraceRenderer.Context>
              */
             final Throwable[] suppressed;
 
+            /**
+             * The cause of this {@link Throwable}.
+             * This needs to be captured separately since {@link Throwable#getCause()} can change.
+             *
+             * @see <a href="https://github.com/apache/logging-log4j2/issues/3929">#4279</a>
+             * @see <a href="https://github.com/apache/logging-log4j2/pull/3934">#4303</a>
+             */
+            @Nullable
+            final Throwable cause;
+
             private Metadata(
                     final int commonElementCount,
                     final int stackLength,
                     final StackTraceElement[] stackTrace,
-                    final Throwable[] suppressed) {
+                    final Throwable[] suppressed,
+                    @Nullable final Throwable cause) {
                 this.commonElementCount = commonElementCount;
                 this.stackLength = stackLength;
                 this.stackTrace = stackTrace;
                 this.suppressed = suppressed;
+                this.cause = cause;
             }
 
             static Map<Throwable, Metadata> ofThrowable(final Throwable throwable) {
@@ -312,8 +324,9 @@ class ThrowableStackTraceRenderer<C extends ThrowableStackTraceRenderer.Context>
                 // Populate metadata of the current throwable
                 @Nullable
                 final StackTraceElement[] rootTrace = parentThrowable == null ? null : parentThrowable.getStackTrace();
+                @Nullable final Throwable cause = throwable.getCause();
                 final Metadata metadata =
-                        populateMetadata(rootTrace, throwable.getStackTrace(), throwable.getSuppressed());
+                        populateMetadata(rootTrace, throwable.getStackTrace(), throwable.getSuppressed(), cause);
                 metadataByThrowable.put(throwable, metadata);
 
                 // Populate metadata of suppressed exceptions
@@ -325,7 +338,6 @@ class ThrowableStackTraceRenderer<C extends ThrowableStackTraceRenderer.Context>
                 }
 
                 // 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);
@@ -335,7 +347,8 @@ class ThrowableStackTraceRenderer<C extends ThrowableStackTraceRenderer.Context>
             private static Metadata populateMetadata(
                     @Nullable final StackTraceElement[] parentTrace,
                     final StackTraceElement[] currentTrace,
-                    final Throwable[] suppressed) {
+                    final Throwable[] suppressed,
+                    @Nullable final Throwable cause) {
                 int commonElementCount;
                 int stackLength;
                 if (parentTrace != null) {
@@ -353,7 +366,7 @@ class ThrowableStackTraceRenderer<C extends ThrowableStackTraceRenderer.Context>
                     commonElementCount = 0;
                     stackLength = currentTrace.length;
                 }
-                return new Metadata(commonElementCount, stackLength, currentTrace, suppressed);
+                return new Metadata(commonElementCount, stackLength, currentTrace, suppressed, cause);
             }
         }
     }
diff --git a/src/changelog/.2.x.x/4279_ThrowableStackTraceRenderer_non_idempotent_getCause.xml b/src/changelog/.2.x.x/4279_ThrowableStackTraceRenderer_non_idempotent_getCause.xml
new file mode 100644
index 0000000000..af5caf8b65
--- /dev/null
+++ b/src/changelog/.2.x.x/4279_ThrowableStackTraceRenderer_non_idempotent_getCause.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="fixed">
+  <issue id="4279" link="https://github.com/apache/logging-log4j2/issues/4279"/>
+  <issue id="4303" link="https://github.com/apache/logging-log4j2/pull/4303"/>
+    <description format="asciidoc">
+      Fix Pattern Layout's stack trace rendering for non-idempotent `Throwable::getCause`
+    </description>
+</entry>