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 59d2be98a4 Recognize nested converters in `alwaysWriteExceptions`
(#3920)
59d2be98a4 is described below
commit 59d2be98a49fa7efd0d66e3d880e5079c104f59c
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Sep 18 19:11:51 2025 +0200
Recognize nested converters in `alwaysWriteExceptions` (#3920)
---
.../log4j/core/pattern/PatternParserTest.java | 47 +++++++++++++++++++++-
.../core/pattern/AbstractStyleNameConverter.java | 7 ++++
.../core/pattern/EncodingPatternConverter.java | 7 ++--
.../pattern/EqualsBaseReplacementConverter.java | 7 ++++
.../log4j/core/pattern/HighlightConverter.java | 9 ++---
.../core/pattern/LogEventPatternConverter.java | 11 +++--
.../log4j/core/pattern/MaxLengthConverter.java | 7 ++++
.../core/pattern/RegexReplacementConverter.java | 7 ++++
.../logging/log4j/core/pattern/StyleConverter.java | 9 ++---
.../VariablesNotEmptyReplacementConverter.java | 7 ++++
src/changelog/.2.x.x/3920-nested-throwables.xml | 12 ++++++
11 files changed, 109 insertions(+), 21 deletions(-)
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java
index 7a82831b96..33a3f2b6ca 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/pattern/PatternParserTest.java
@@ -44,6 +44,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
class PatternParserTest {
@@ -93,6 +94,50 @@ class PatternParserTest {
validateConverter(formatters, 1, "Line Sep");
}
+ @ParameterizedTest
+ @ValueSource(strings = {"%m", "%p", "%c", "%d"})
+ void testAlwaysWriteExceptions_appendsThrowable(String pattern) {
+ // With alwaysWriteExceptions=true, parser should auto-append a
%throwable
+ final List<PatternFormatter> formatters = parser.parse(pattern + "%n",
true, false, false);
+ assertThat(formatters).hasSize(3);
+
assertThat(formatters.get(2).getConverter()).isInstanceOf(ThrowablePatternConverter.class);
+
+ // With alwaysWriteExceptions=false, parser should leave the pattern
unchanged
+ final List<PatternFormatter> formatters2 = parser.parse(pattern +
"%n", false, false, false);
+ assertThat(formatters2).hasSize(2);
+
assertThat(formatters2.get(1).getConverter()).isNotInstanceOf(ThrowablePatternConverter.class);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "%black{%throwable}",
+ "%blue{%throwable}",
+ "%cyan{%throwable}",
+ "%green{%throwable}",
+ "%magenta{%throwable}",
+ "%red{%throwable}",
+ "%white{%throwable}",
+ "%yellow{%throwable}",
+ "%encode{%throwable}{JSON}",
+ "%equals{%throwable}{java.lang.Throwable}{T}",
+ "%equalsIgnoreCase{%throwable}{java.lang.Throwable}{T}",
+ "%highlight{%throwable}",
+ "%maxLen{%throwable}{1024}",
+ "%replace{%throwable}{\n}{ }",
+ "%style{%throwable}{red bold}",
+ "%notEmpty{%throwable}",
+ })
+ void testAlwaysWriteExceptions_recognizesNestedPatterns(String pattern) {
+ // With alwaysWriteExceptions=true, parser must detect the nested
%throwable
+ // and NOT auto-append another one at the top level
+ final List<PatternFormatter> formatters = parser.parse(pattern, true,
false, false);
+
+ // Only one top-level formatter is expected (the wrapper itself), not
a trailing ThrowablePatternConverter
+ assertThat(formatters).hasSize(1);
+
assertThat(formatters.get(0).getConverter()).isNotInstanceOf(ThrowablePatternConverter.class);
+ }
+
/**
* Test the custom pattern
*/
@@ -103,7 +148,7 @@ class PatternParserTest {
final StringMap mdc = ContextDataFactory.createContextData();
mdc.putValue("loginId", "Fred");
// The line number of the Throwable definition
- final int nextLineNumber = 107;
+ final int nextLineNumber = 152; // Adjust this when the code above
changes!
final Throwable t = new Throwable();
final StackTraceElement[] elements = t.getStackTrace();
final Log4jLogEvent event = Log4jLogEvent.newBuilder() //
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AbstractStyleNameConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AbstractStyleNameConverter.java
index b7b5e35018..98d69acade 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AbstractStyleNameConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/AbstractStyleNameConverter.java
@@ -376,4 +376,11 @@ public abstract class AbstractStyleNameConverter extends
LogEventPatternConverte
toAppendTo.append(AnsiEscape.getDefaultStyle());
}
}
+
+ @Override
+ public boolean handlesThrowable() {
+ return formatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
+ }
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EncodingPatternConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EncodingPatternConverter.java
index 970517f69f..2458e2cb70 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EncodingPatternConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EncodingPatternConverter.java
@@ -54,10 +54,9 @@ public final class EncodingPatternConverter extends
LogEventPatternConverter {
@Override
public boolean handlesThrowable() {
- return formatters != null
- && formatters.stream()
- .map(PatternFormatter::getConverter)
- .anyMatch(LogEventPatternConverter::handlesThrowable);
+ return formatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
}
/**
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EqualsBaseReplacementConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EqualsBaseReplacementConverter.java
index 904d498169..e71edb6147 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EqualsBaseReplacementConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/EqualsBaseReplacementConverter.java
@@ -99,4 +99,11 @@ public abstract class EqualsBaseReplacementConverter extends
LogEventPatternConv
substitutionBuffer.append(substitution);
}
}
+
+ @Override
+ public boolean handlesThrowable() {
+ return formatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
+ }
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/HighlightConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/HighlightConverter.java
index 29663b4f5a..6f72d2f47e 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/HighlightConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/HighlightConverter.java
@@ -265,11 +265,8 @@ public final class HighlightConverter extends
LogEventPatternConverter implement
@Override
public boolean handlesThrowable() {
- for (final PatternFormatter formatter : patternFormatters) {
- if (formatter.handlesThrowable()) {
- return true;
- }
- }
- return false;
+ return patternFormatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
}
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/LogEventPatternConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/LogEventPatternConverter.java
index 4d4bc2cbc7..4c175abdb4 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/LogEventPatternConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/LogEventPatternConverter.java
@@ -53,13 +53,16 @@ public abstract class LogEventPatternConverter extends
AbstractPatternConverter
}
/**
- * Normally pattern converters are not meant to handle Exceptions although
few pattern converters might.
+ * Tests whether this pattern converter is renders a {@link Throwable}.
+ *
* <p>
- * By examining the return values for this method, the containing layout
will determine whether it handles
- * throwables or not.
+ * The {@link PatternParser} checks this flag when processing the
+ * {@code alwaysWriteExceptions} option: if no converter in the pattern
handles
+ * throwables, the parser automatically appends a converter to ensure
exceptions are still written.
* </p>
*
- * @return true if this PatternConverter handles throwables
+ * @return {@code true} if this converter consumes and renders a {@link
Throwable},
+ * {@code false} otherwise
*/
public boolean handlesThrowable() {
return false;
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MaxLengthConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MaxLengthConverter.java
index 1a2805c23b..432cd4f61f 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MaxLengthConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/MaxLengthConverter.java
@@ -99,4 +99,11 @@ public final class MaxLengthConverter extends
LogEventPatternConverter {
}
}
}
+
+ @Override
+ public boolean handlesThrowable() {
+ return formatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
+ }
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RegexReplacementConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RegexReplacementConverter.java
index 4a21a753bb..27bd86ada7 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RegexReplacementConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/RegexReplacementConverter.java
@@ -92,4 +92,11 @@ public final class RegexReplacementConverter extends
LogEventPatternConverter {
}
toAppendTo.append(pattern.matcher(buf.toString()).replaceAll(substitution));
}
+
+ @Override
+ public boolean handlesThrowable() {
+ return formatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
+ }
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/StyleConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/StyleConverter.java
index 40267f6155..d6c0d123d8 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/StyleConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/StyleConverter.java
@@ -129,12 +129,9 @@ public final class StyleConverter extends
LogEventPatternConverter implements An
@Override
public boolean handlesThrowable() {
- for (final PatternFormatter formatter : patternFormatters) {
- if (formatter.handlesThrowable()) {
- return true;
- }
- }
- return false;
+ return patternFormatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
}
/**
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/VariablesNotEmptyReplacementConverter.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/VariablesNotEmptyReplacementConverter.java
index 7fda5791d5..ca7cdf71e5 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/VariablesNotEmptyReplacementConverter.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/VariablesNotEmptyReplacementConverter.java
@@ -119,4 +119,11 @@ public final class VariablesNotEmptyReplacementConverter
extends LogEventPattern
}
return true;
}
+
+ @Override
+ public boolean handlesThrowable() {
+ return formatters.stream()
+ .map(PatternFormatter::getConverter)
+ .anyMatch(LogEventPatternConverter::handlesThrowable);
+ }
}
diff --git a/src/changelog/.2.x.x/3920-nested-throwables.xml
b/src/changelog/.2.x.x/3920-nested-throwables.xml
new file mode 100644
index 0000000000..a8c9b13cfe
--- /dev/null
+++ b/src/changelog/.2.x.x/3920-nested-throwables.xml
@@ -0,0 +1,12 @@
+<?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="3920" link="https://github.com/apache/logging-log4j2/pull/3920"/>
+ <description format="asciidoc">
+ Fix detection of throwable converters inside nested patterns when applying
`alwaysWriteExceptions`.
+ </description>
+</entry>