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 <pkarwasz-git...@apache.org> 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>