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>