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 2022/08/17 09:30:54 UTC

[logging-log4j2] branch LOG4J2-3556 created (now 100c6043d4)

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

vy pushed a change to branch LOG4J2-3556
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


      at 100c6043d4 LOG4J2-3556 Rearrange stringified stack trace tests.

This branch includes the following new commits:

     new 498fa4f12b LOG4J2-3556 Make JsonTemplateLayout stack trace truncation operate for each label block.
     new a0c24a5a39 LOG4J2-3556 Fix stack trace truncation matcher regex.
     new 100c6043d4 LOG4J2-3556 Rearrange stringified stack trace tests.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[logging-log4j2] 03/03: LOG4J2-3556 Rearrange stringified stack trace tests.

Posted by vy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 100c6043d4476e8817e387e990d9ff457f850370
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Wed Aug 17 11:26:55 2022 +0200

    LOG4J2-3556 Rearrange stringified stack trace tests.
---
 .../template/json/JsonTemplateLayoutTest.java      | 276 +--------------
 .../resolver/StackTraceStringResolverTest.java     | 374 ++++++++++++++++-----
 2 files changed, 311 insertions(+), 339 deletions(-)

diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
index 2cd2518872..f0b8b86d3e 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
@@ -34,31 +34,14 @@ import org.apache.logging.log4j.core.lookup.MainMapLookup;
 import org.apache.logging.log4j.core.net.Severity;
 import org.apache.logging.log4j.core.time.MutableInstant;
 import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout.EventTemplateAdditionalField;
-import org.apache.logging.log4j.layout.template.json.resolver.EventResolver;
-import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
-import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
-import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
-import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
-import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
+import org.apache.logging.log4j.layout.template.json.resolver.*;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.message.Message;
-import org.apache.logging.log4j.message.MessageFactory;
-import org.apache.logging.log4j.message.ObjectMessage;
-import org.apache.logging.log4j.message.ParameterizedMessageFactory;
-import org.apache.logging.log4j.message.ReusableMessageFactory;
-import org.apache.logging.log4j.message.SimpleMessage;
-import org.apache.logging.log4j.message.StringMapMessage;
+import org.apache.logging.log4j.message.*;
 import org.apache.logging.log4j.test.AvailablePortFinder;
 import org.apache.logging.log4j.util.Strings;
-import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
 import java.math.BigDecimal;
 import java.net.ServerSocket;
 import java.net.Socket;
@@ -67,11 +50,7 @@ import java.nio.charset.Charset;
 import java.time.Instant;
 import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAccessor;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -81,6 +60,7 @@ import java.util.stream.IntStream;
 
 import static org.apache.logging.log4j.layout.template.json.TestHelpers.*;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 @SuppressWarnings("DoubleBraceInitialization")
 class JsonTemplateLayoutTest {
@@ -770,85 +750,17 @@ class JsonTemplateLayoutTest {
 
     }
 
-    private static final class NonAsciiUtf8MethodNameContainingException extends RuntimeException {
-
-        public static final long serialVersionUID = 0;
-
-        private static final String NON_ASCII_UTF8_TEXT = "அஆஇฬ๘";
-
-        private static final NonAsciiUtf8MethodNameContainingException INSTANCE =
-                createInstance();
-
-        private static NonAsciiUtf8MethodNameContainingException createInstance() {
-            try {
-                throwException_அஆஇฬ๘();
-                throw new IllegalStateException("should not have reached here");
-            } catch (final NonAsciiUtf8MethodNameContainingException exception) {
-                return exception;
-            }
-        }
-
-        @SuppressWarnings("NonAsciiCharacters")
-        private static void throwException_அஆஇฬ๘() {
-            throw new NonAsciiUtf8MethodNameContainingException(
-                    "exception with non-ASCII UTF-8 method name");
-        }
-
-        private NonAsciiUtf8MethodNameContainingException(final String message) {
-            super(message);
-        }
-
-    }
-
-    @Test
-    void test_exception_with_nonAscii_utf8_method_name() {
-
-        // Create the log event.
-        final SimpleMessage message = new SimpleMessage("Hello, World!");
-        final RuntimeException exception = NonAsciiUtf8MethodNameContainingException.INSTANCE;
-        final LogEvent logEvent = Log4jLogEvent
-                .newBuilder()
-                .setLoggerName(LOGGER_NAME)
-                .setLevel(Level.ERROR)
-                .setMessage(message)
-                .setThrown(exception)
-                .build();
-
-        // Create the event template.
-        final String eventTemplate = writeJson(asMap(
-                "ex_stacktrace", asMap(
-                        "$resolver", "exception",
-                        "field", "stackTrace",
-                        "stringified", true)));
-
-        // Create the layout.
-        final JsonTemplateLayout layout = JsonTemplateLayout
-                .newBuilder()
-                .setConfiguration(CONFIGURATION)
-                .setStackTraceEnabled(true)
-                .setEventTemplate(eventTemplate)
-                .build();
-
-        // Check the serialized event.
-        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
-                assertThat(accessor.getString("ex_stacktrace"))
-                        .contains(NonAsciiUtf8MethodNameContainingException.NON_ASCII_UTF8_TEXT));
-
-    }
-
     @Test
     void test_event_template_additional_fields() {
 
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
-        final RuntimeException exception = NonAsciiUtf8MethodNameContainingException.INSTANCE;
         final Level level = Level.ERROR;
         final LogEvent logEvent = Log4jLogEvent
                 .newBuilder()
                 .setLoggerName(LOGGER_NAME)
                 .setLevel(level)
                 .setMessage(message)
-                .setThrown(exception)
                 .build();
 
         // Create the event template.
@@ -971,8 +883,7 @@ class JsonTemplateLayoutTest {
             // Verify the test case.
             usingSerializedLogEventAccessor(layout, logEvent, accessor ->
                     testCase.forEach((key, expectedValue) ->
-                            Assertions
-                                    .assertThat(accessor.getObject(key))
+                            assertThat(accessor.getObject(key))
                                     .describedAs("key=%s", key)
                                     .isEqualTo(expectedValue)));
 
@@ -1139,161 +1050,6 @@ class JsonTemplateLayoutTest {
 
     }
 
-    @Test
-    void test_stringified_exception_resolver_with_maxStringLength() {
-
-        // Create the event template.
-        final String eventTemplate = writeJson(asMap(
-                "stackTrace", asMap(
-                        "$resolver", "exception",
-                        "field", "stackTrace",
-                        "stringified", true)));
-
-        // Create the layout.
-        final int maxStringLength = eventTemplate.length();
-        final JsonTemplateLayout layout = JsonTemplateLayout
-                .newBuilder()
-                .setConfiguration(CONFIGURATION)
-                .setEventTemplate(eventTemplate)
-                .setMaxStringLength(maxStringLength)
-                .setStackTraceEnabled(true)
-                .build();
-
-        // Create the log event.
-        final SimpleMessage message = new SimpleMessage("foo");
-        final LogEvent logEvent = Log4jLogEvent
-                .newBuilder()
-                .setLoggerName(LOGGER_NAME)
-                .setMessage(message)
-                .setThrown(NonAsciiUtf8MethodNameContainingException.INSTANCE)
-                .build();
-
-        // Check the serialized event.
-        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
-            final int expectedLength = maxStringLength +
-                    JsonTemplateLayoutDefaults.getTruncatedStringSuffix().length();
-            assertThat(accessor.getString("stackTrace").length()).isEqualTo(expectedLength);
-        });
-
-    }
-
-    @Test
-    void test_stack_trace_truncation() {
-
-        // Create the exception to be logged.
-        final Exception childError =
-                new Exception("unique child exception message");
-        final Exception parentError =
-                new Exception("unique parent exception message", childError);
-
-        // Create the event template.
-        final String truncationSuffix = "~";
-        final String eventTemplate = writeJson(asMap(
-                // Raw exception.
-                "ex", asMap(
-                        "$resolver", "exception",
-                        "field", "stackTrace",
-                        "stackTrace", asMap(
-                                "stringified", true)),
-                // Exception matcher using strings.
-                "stringMatchedEx", asMap(
-                        "$resolver", "exception",
-                        "field", "stackTrace",
-                        "stackTrace", asMap(
-                                "stringified", asMap(
-                                        "truncation", asMap(
-                                                "suffix", truncationSuffix,
-                                                "pointMatcherStrings", Arrays.asList(
-                                                        "this string shouldn't match with anything",
-                                                        parentError.getMessage()))))),
-                // Exception matcher using regexes.
-                "regexMatchedEx", asMap(
-                        "$resolver", "exception",
-                        "field", "stackTrace",
-                        "stackTrace", asMap(
-                                "stringified", asMap(
-                                        "truncation", asMap(
-                                                "suffix", truncationSuffix,
-                                                "pointMatcherRegexes", Arrays.asList(
-                                                        "this string shouldn't match with anything",
-                                                        parentError
-                                                                .getMessage()
-                                                                .replace("unique", "[xu]n.que")))))),
-                // Raw exception root cause.
-                "rootEx", asMap(
-                        "$resolver", "exceptionRootCause",
-                        "field", "stackTrace",
-                        "stackTrace", asMap(
-                                "stringified", true)),
-                // Exception root cause matcher using strings.
-                "stringMatchedRootEx", asMap(
-                        "$resolver", "exceptionRootCause",
-                        "field", "stackTrace",
-                        "stackTrace", asMap(
-                                "stringified", asMap(
-                                        "truncation", asMap(
-                                                "suffix", truncationSuffix,
-                                                "pointMatcherStrings", Arrays.asList(
-                                                        "this string shouldn't match with anything",
-                                                        childError.getMessage()))))),
-                // Exception root cause matcher using regexes.
-                "regexMatchedRootEx", asMap(
-                        "$resolver", "exceptionRootCause",
-                        "field", "stackTrace",
-                        "stackTrace", asMap(
-                                "stringified", asMap(
-                                        "truncation", asMap(
-                                                "suffix", truncationSuffix,
-                                                "pointMatcherRegexes", Arrays.asList(
-                                                        "this string shouldn't match with anything",
-                                                        childError
-                                                                .getMessage()
-                                                                .replace("unique", "[xu]n.que"))))))));
-
-        // Create the layout.
-        final JsonTemplateLayout layout = JsonTemplateLayout
-                .newBuilder()
-                .setConfiguration(CONFIGURATION)
-                .setEventTemplate(eventTemplate)
-                .setStackTraceEnabled(true)
-                .build();
-
-        // Create the log event.
-        final LogEvent logEvent = Log4jLogEvent
-                .newBuilder()
-                .setLoggerName(LOGGER_NAME)
-                .setThrown(parentError)
-                .build();
-
-        // Check the serialized event.
-        final String expectedMatchedExEnd =
-                parentError.getMessage() + truncationSuffix;
-        final String expectedMatchedRootExEnd =
-                childError.getMessage() + truncationSuffix;
-        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
-
-            // Check the serialized exception.
-            assertThat(accessor.getString("ex"))
-                    .doesNotEndWith(expectedMatchedExEnd)
-                    .doesNotEndWith(expectedMatchedRootExEnd);
-            assertThat(accessor.getString("stringMatchedEx"))
-                    .endsWith(expectedMatchedExEnd);
-            assertThat(accessor.getString("regexMatchedEx"))
-                    .endsWith(expectedMatchedExEnd);
-
-            // Check the serialized exception root cause.
-            assertThat(accessor.getString("rootEx"))
-                    .doesNotEndWith(expectedMatchedExEnd)
-                    .doesNotEndWith(expectedMatchedRootExEnd);
-            assertThat(accessor.getString("stringMatchedRootEx"))
-                    .endsWith(expectedMatchedRootExEnd);
-            assertThat(accessor.getString("regexMatchedRootEx"))
-                    .endsWith(expectedMatchedRootExEnd);
-
-        });
-
-    }
-
     @Test
     void test_inline_stack_trace_element_template() {
 
@@ -1326,9 +1082,9 @@ class JsonTemplateLayoutTest {
 
         // Check the serialized log event.
         final String expectedClassName = JsonTemplateLayoutTest.class.getCanonicalName();
-        usingSerializedLogEventAccessor(layout, logEvent, accessor -> Assertions
-                .assertThat(accessor.getList("stackTrace", String.class))
-                .contains(expectedClassName));
+        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                assertThat(accessor.getList("stackTrace", String.class))
+                        .contains(expectedClassName));
 
     }
 
@@ -1355,9 +1111,9 @@ class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized log event.
-        usingSerializedLogEventAccessor(layout, logEvent, accessor -> Assertions
-                .assertThat(accessor.getString("customField"))
-                .matches("CustomValue-[0-9]+"));
+        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                assertThat(accessor.getString("customField"))
+                        .matches("CustomValue-[0-9]+"));
 
     }
 
@@ -1422,7 +1178,6 @@ class JsonTemplateLayoutTest {
                 .newBuilder()
                 .setLoggerName(LOGGER_NAME)
                 .setMessage(message)
-                .setThrown(NonAsciiUtf8MethodNameContainingException.INSTANCE)
                 .build();
 
         // Check the serialized event.
@@ -1814,9 +1569,7 @@ class JsonTemplateLayoutTest {
         final String expectedSerializedLogEventJson =
                 "{}" + JsonTemplateLayoutDefaults.getEventDelimiter();
         final String actualSerializedLogEventJson = layout.toSerializable(logEvent);
-        Assertions
-                .assertThat(actualSerializedLogEventJson)
-                .isEqualTo(expectedSerializedLogEventJson);
+        assertThat(actualSerializedLogEventJson).isEqualTo(expectedSerializedLogEventJson);
 
     }
 
@@ -1847,8 +1600,7 @@ class JsonTemplateLayoutTest {
                 .build();
 
         // Check the serialized event.
-        Assertions
-                .assertThatThrownBy(() -> layout.toSerializable(logEvent))
+        assertThatThrownBy(() -> layout.toSerializable(logEvent))
                 .isInstanceOf(StackOverflowError.class);
 
     }
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java
index c1aa485e21..6c93b1902f 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java
@@ -19,8 +19,8 @@ package org.apache.logging.log4j.layout.template.json.resolver;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.impl.Log4jLogEvent;
 import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
 import org.assertj.core.api.AbstractStringAssert;
-import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 
@@ -28,10 +28,12 @@ import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.math.BigDecimal;
 import java.net.ServerSocket;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -47,7 +49,7 @@ class StackTraceStringResolverTest {
     // Below we create arbitrary exceptions containing stack entries from non-Log4j packages.
     // Non-Log4j package origin is needed to avoid the truncation (e.g., `... 58 more`) done by `Throwable#printStackTrace()`.
 
-    private static final String EXCEPTION_REGEX_FLAGS = "(?m)(?s)";     // MULTILINE | DOTALL
+    private static final String EXCEPTION_REGEX_FLAGS = "(?s)";     // DOTALL
 
     private static final String TRUNCATION_SUFFIX = "<truncated>";
 
@@ -110,9 +112,7 @@ class StackTraceStringResolverTest {
         final Throwable error = exception1();
         final String stackTrace = stackTrace(error);
         final String regex = exception1Regex(false);
-        Assertions
-                .assertThat(stackTrace)
-                .matches(EXCEPTION_REGEX_FLAGS + regex);
+        assertThat(stackTrace).matches(EXCEPTION_REGEX_FLAGS + regex);
     }
 
     @Test
@@ -120,9 +120,7 @@ class StackTraceStringResolverTest {
         final Throwable error = exception2();
         final String stackTrace = stackTrace(error);
         final String regex = exception2Regex(false);
-        Assertions
-                .assertThat(stackTrace)
-                .matches(EXCEPTION_REGEX_FLAGS + regex);
+        assertThat(stackTrace).matches(EXCEPTION_REGEX_FLAGS + regex);
     }
 
     @Test
@@ -130,9 +128,7 @@ class StackTraceStringResolverTest {
         final Throwable error = exception3();
         final String stackTrace = stackTrace(error);
         final String regex = exception3Regex(false);
-        Assertions
-                .assertThat(stackTrace)
-                .matches(EXCEPTION_REGEX_FLAGS + regex);
+        assertThat(stackTrace).matches(EXCEPTION_REGEX_FLAGS + regex);
     }
 
     private static String stackTrace(final Throwable throwable) {
@@ -309,6 +305,35 @@ class StackTraceStringResolverTest {
                 final Throwable exception,
                 final String regex);
 
+        private static void assertSerializedException(
+                final Map<String, ?> exceptionResolverTemplate,
+                final Throwable exception,
+                final Consumer<AbstractStringAssert<?>> serializedExceptionAsserter) {
+
+            // Create the event template.
+            final String eventTemplate = writeJson(asMap("output", exceptionResolverTemplate));
+
+            // Create the layout.
+            final JsonTemplateLayout layout = JsonTemplateLayout
+                    .newBuilder()
+                    .setConfiguration(CONFIGURATION)
+                    .setEventTemplate(eventTemplate)
+                    .build();
+
+            // Create the log event.
+            final LogEvent logEvent = Log4jLogEvent
+                    .newBuilder()
+                    .setThrown(exception)
+                    .build();
+
+            // Check the serialized event.
+            usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+                AbstractStringAssert<?> serializedExceptionAssert = assertThat(accessor.getString("output"));
+                serializedExceptionAsserter.accept(serializedExceptionAssert);
+            });
+
+        }
+
     }
 
     ////////////////////////////////////////////////////////////////////////////
@@ -327,23 +352,59 @@ class StackTraceStringResolverTest {
             assertSerializedExceptionWithoutTruncation(exception, regex);
         }
 
-    }
+        private void assertSerializedExceptionWithoutTruncation(
+                final Throwable exception,
+                final String regex) {
 
-    private static void assertSerializedExceptionWithoutTruncation(
-            final Throwable exception,
-            final String regex) {
+            // Create the event template.
+            final Map<String, ?> exceptionResolverTemplate = asMap(
+                    "$resolver", "exception",
+                    "field", "stackTrace",
+                    "stackTrace", asMap("stringified", true));
 
-        // Create the event template.
-        final Map<String, ?> exceptionResolverTemplate = asMap(
-                "$resolver", "exception",
-                "field", "stackTrace",
-                "stackTrace", asMap("stringified", true));
+            // Check the serialized event.
+            AbstractTestCases.assertSerializedException(
+                    exceptionResolverTemplate,
+                    exception,
+                    serializedExceptionAssert -> serializedExceptionAssert.matches(regex));
 
-        // Check the serialized event.
-        assertSerializedException(
-                exceptionResolverTemplate,
-                exception,
-                serializedExceptionAssert -> serializedExceptionAssert.matches(regex));
+        }
+
+        @Test
+        void JsonWriter_maxStringLength_should_work() {
+
+            // Create the event template.
+            final String eventTemplate = writeJson(asMap(
+                    "ex", asMap(
+                            "$resolver", "exception",
+                            "field", "stackTrace",
+                            "stringified", true)));
+
+            // Create the layout.
+            final int maxStringLength = eventTemplate.length();
+            final JsonTemplateLayout layout = JsonTemplateLayout
+                    .newBuilder()
+                    .setConfiguration(CONFIGURATION)
+                    .setEventTemplate(eventTemplate)
+                    .setMaxStringLength(maxStringLength)
+                    .setStackTraceEnabled(true)
+                    .build();
+
+            // Create the log event.
+            Throwable exception = exception1();
+            final LogEvent logEvent = Log4jLogEvent
+                    .newBuilder()
+                    .setThrown(exception)
+                    .build();
+
+            // Check the serialized event.
+            usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+                final int expectedLength = maxStringLength +
+                        JsonTemplateLayoutDefaults.getTruncatedStringSuffix().length();
+                assertThat(accessor.getString("ex").length()).isEqualTo(expectedLength);
+            });
+
+        }
 
     }
 
@@ -352,9 +413,9 @@ class StackTraceStringResolverTest {
     ////////////////////////////////////////////////////////////////////////////
 
     @Nested
-    class WithStringTruncation extends AbstractTestCases {
+    class WithTruncation extends AbstractTestCases {
 
-        WithStringTruncation() {
+        WithTruncation() {
             super(true);
         }
 
@@ -362,75 +423,234 @@ class StackTraceStringResolverTest {
         void assertSerializedException(final Throwable exception, final String regex) {
             assertSerializedExceptionWithStringTruncation(exception, regex);
         }
-        
-    }
 
-    private static void assertSerializedExceptionWithStringTruncation(
-            final Throwable exception,
-            final String regex) {
+        private void assertSerializedExceptionWithStringTruncation(
+                final Throwable exception,
+                final String regex) {
+
+            // Create the event template.
+            final List<String> pointMatcherStrings = pointMatcherStrings();
+            final Map<String, ?> exceptionResolverTemplate = asMap(
+                    "$resolver", "exception",
+                    "field", "stackTrace",
+                    "stackTrace", asMap("stringified", asMap(
+                            "truncation", asMap(
+                                    "suffix", TRUNCATION_SUFFIX,
+                                    "pointMatcherStrings", pointMatcherStrings))));
+
+            // Check the serialized event.
+            AbstractTestCases.assertSerializedException(
+                    exceptionResolverTemplate,
+                    exception,
+                    serializedExceptionAssert -> serializedExceptionAssert.matches(regex));
 
-        // Create the event template.
-        final List<String> pointMatcherStrings = pointMatcherStrings();
-        final Map<String, ?> exceptionResolverTemplate = asMap(
-                "$resolver", "exception",
-                "field", "stackTrace",
-                "stackTrace", asMap("stringified", asMap(
-                        "truncation", asMap(
-                                "suffix", TRUNCATION_SUFFIX,
-                                "pointMatcherStrings", pointMatcherStrings))));
+        }
 
-        // Check the serialized event.
-        assertSerializedException(
-                exceptionResolverTemplate,
-                exception,
-                serializedExceptionAssert -> serializedExceptionAssert.matches(regex));
+        private List<String> pointMatcherStrings() {
+            final Throwable exception1 = exception1();
+            final Throwable exception2 = exception2();
+            final Throwable exception3 = exception3();
+            return Stream
+                    .of(exception1, exception2, exception3)
+                    .map(this::pointMatcherString)
+                    .collect(Collectors.toList());
+        }
 
-    }
+        @Test
+        void point_matchers_should_work() {
+
+            // Create the exception to be logged.
+            final Throwable parentError = exception1();
+            final Throwable childError = exception3();
+            parentError.initCause(childError);
+
+            // Create the event template.
+            final String eventTemplate = writeJson(asMap(
+
+                    // Raw exception
+                    "ex", asMap(
+                            "$resolver", "exception",
+                            "field", "stackTrace",
+                            "stackTrace", asMap(
+                                    "stringified", true)),
+
+                    // Exception matcher using strings
+                    "stringMatchedEx", asMap(
+                            "$resolver", "exception",
+                            "field", "stackTrace",
+                            "stackTrace", asMap(
+                                    "stringified", asMap(
+                                            "truncation", asMap(
+                                                    "suffix", TRUNCATION_SUFFIX,
+                                                    "pointMatcherStrings", Arrays.asList(
+                                                            "this string shouldn't match with anything",
+                                                            pointMatcherString(parentError)))))),
+
+                    // Exception matcher using regexes
+                    "regexMatchedEx", asMap(
+                            "$resolver", "exception",
+                            "field", "stackTrace",
+                            "stackTrace", asMap(
+                                    "stringified", asMap(
+                                            "truncation", asMap(
+                                                    "suffix", TRUNCATION_SUFFIX,
+                                                    "pointMatcherRegexes", Arrays.asList(
+                                                            "this string shouldn't match with anything",
+                                                            pointMatcherRegex(parentError)))))),
+
+                    // Raw exception root cause
+                    "rootEx", asMap(
+                            "$resolver", "exceptionRootCause",
+                            "field", "stackTrace",
+                            "stackTrace", asMap(
+                                    "stringified", true)),
+
+                    // Exception root cause matcher using strings
+                    "stringMatchedRootEx", asMap(
+                            "$resolver", "exceptionRootCause",
+                            "field", "stackTrace",
+                            "stackTrace", asMap(
+                                    "stringified", asMap(
+                                            "truncation", asMap(
+                                                    "suffix", TRUNCATION_SUFFIX,
+                                                    "pointMatcherStrings", Arrays.asList(
+                                                            "this string shouldn't match with anything",
+                                                            pointMatcherString(childError)))))),
+
+                    // Exception root cause matcher using regexes
+                    "regexMatchedRootEx", asMap(
+                            "$resolver", "exceptionRootCause",
+                            "field", "stackTrace",
+                            "stackTrace", asMap(
+                                    "stringified", asMap(
+                                            "truncation", asMap(
+                                                    "suffix", TRUNCATION_SUFFIX,
+                                                    "pointMatcherRegexes", Arrays.asList(
+                                                            "this string shouldn't match with anything",
+                                                            pointMatcherRegex(childError))))))));
+
+            // Create the layout.
+            final JsonTemplateLayout layout = JsonTemplateLayout
+                    .newBuilder()
+                    .setConfiguration(CONFIGURATION)
+                    .setEventTemplate(eventTemplate)
+                    .build();
+
+            // Create the log event.
+            final LogEvent logEvent = Log4jLogEvent
+                    .newBuilder()
+                    .setThrown(parentError)
+                    .build();
+
+            // Check the serialized event.
+            usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+
+                // Check the raw parent exception.
+                final String exPattern = EXCEPTION_REGEX_FLAGS +
+                        exception1Regex(false) +
+                        "\nCaused by: " + exception3Regex(false);
+                assertThat(accessor.getString("ex")).matches(exPattern);
+
+                // Check the matcher usage on parent exception.
+                final String matchedExPattern = EXCEPTION_REGEX_FLAGS +
+                        exception1Regex(true) +
+                        "\nCaused by: " + exception3Regex(false);
+                assertThat(accessor.getString("stringMatchedEx")).matches(matchedExPattern);
+                assertThat(accessor.getString("regexMatchedEx")).matches(matchedExPattern);
+
+                // Check the raw child exception.
+                final String rootExPattern = EXCEPTION_REGEX_FLAGS +
+                        exception3Regex(false);
+                assertThat(accessor.getString("rootEx")).matches(rootExPattern);
+
+                // Check the matcher usage on child exception.
+                final String matchedRootExPattern = EXCEPTION_REGEX_FLAGS +
+                        exception3Regex(true);
+                assertThat(accessor.getString("stringMatchedRootEx")).matches(matchedRootExPattern);
+                assertThat(accessor.getString("regexMatchedRootEx")).matches(matchedRootExPattern);
+
+            });
+
+        }
+
+        private String pointMatcherString(Throwable exception) {
+            final StackTraceElement stackTraceElement = exception.getStackTrace()[0];
+            final String className = stackTraceElement.getClassName();
+            return "at " + className;
+        }
+
+        private String pointMatcherRegex(Throwable exception) {
+            String string = pointMatcherString(exception);
+            return matchingRegex(string);
+        }
 
-    private static List<String> pointMatcherStrings() {
-        final Throwable exception1 = exception1();
-        final Throwable exception2 = exception2();
-        final Throwable exception3 = exception3();
-        return Stream
-                .of(exception1, exception2, exception3)
-                .map(exception -> {
-                    final StackTraceElement stackTraceElement = exception.getStackTrace()[0];
-                    final String className = stackTraceElement.getClassName();
-                    return "at " + className;
-                })
-                .collect(Collectors.toList());
+        /**
+         * @return a regex matching the given input
+         */
+        private String matchingRegex(String string) {
+            return "[" + string.charAt(0) + "]" + Pattern.quote(string.substring(1));
+        }
+        
     }
 
-    ////////////////////////////////////////////////////////////////////////////
-    // utilities ///////////////////////////////////////////////////////////////
-    ////////////////////////////////////////////////////////////////////////////
+    @Test
+    void nonAscii_utf8_method_name_should_get_serialized() {
 
-    private static void assertSerializedException(
-            final Map<String, ?> exceptionResolverTemplate,
-            final Throwable exception,
-            final Consumer<AbstractStringAssert<?>> serializedExceptionAsserter) {
+        // Create the log event.
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setThrown(NonAsciiUtf8MethodNameContainingException.INSTANCE)
+                .build();
 
         // Create the event template.
-        final String eventTemplate = writeJson(asMap("output", exceptionResolverTemplate));
+        final String eventTemplate = writeJson(asMap(
+                "ex_stacktrace", asMap(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true)));
 
         // Create the layout.
         final JsonTemplateLayout layout = JsonTemplateLayout
                 .newBuilder()
                 .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
                 .setEventTemplate(eventTemplate)
                 .build();
 
-        // Create the log event.
-        final LogEvent logEvent = Log4jLogEvent
-                .newBuilder()
-                .setThrown(exception)
-                .build();
-
         // Check the serialized event.
-        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
-            AbstractStringAssert<?> serializedExceptionAssert = assertThat(accessor.getString("output"));
-            serializedExceptionAsserter.accept(serializedExceptionAssert);
-        });
+        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                assertThat(accessor.getString("ex_stacktrace"))
+                        .contains(NonAsciiUtf8MethodNameContainingException.NON_ASCII_UTF8_TEXT));
+
+    }
+
+    private static final class NonAsciiUtf8MethodNameContainingException extends RuntimeException {
+
+        public static final long serialVersionUID = 0;
+
+        private static final String NON_ASCII_UTF8_TEXT = "அஆஇฬ๘";
+
+        private static final NonAsciiUtf8MethodNameContainingException INSTANCE =
+                createInstance();
+
+        private static NonAsciiUtf8MethodNameContainingException createInstance() {
+            try {
+                throwException_அஆஇฬ๘();
+                throw new IllegalStateException("should not have reached here");
+            } catch (final NonAsciiUtf8MethodNameContainingException exception) {
+                return exception;
+            }
+        }
+
+        @SuppressWarnings("NonAsciiCharacters")
+        private static void throwException_அஆஇฬ๘() {
+            throw new NonAsciiUtf8MethodNameContainingException(
+                    "exception with non-ASCII UTF-8 method name");
+        }
+
+        private NonAsciiUtf8MethodNameContainingException(final String message) {
+            super(message);
+        }
 
     }
 


[logging-log4j2] 01/03: LOG4J2-3556 Make JsonTemplateLayout stack trace truncation operate for each label block.

Posted by vy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 498fa4f12b7c3a2dbba19d5c86101f31ecab1d3b
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Mon Aug 15 23:01:45 2022 +0200

    LOG4J2-3556 Make JsonTemplateLayout stack trace truncation operate for each label block.
---
 .../template/json/resolver/ExceptionResolver.java  |   9 +-
 .../json/resolver/StackTraceStringResolver.java    | 242 ++++++++++--
 .../template/json/util/CharSequencePointer.java    | 106 +++++
 .../json/util/TruncatingBufferedPrintWriter.java   |  17 +-
 .../json/util/TruncatingBufferedWriter.java        |  50 +--
 .../resolver/StackTraceStringResolverTest.java     | 437 +++++++++++++++++++++
 .../json/util/CharSequencePointerTest.java         | 121 ++++++
 .../json/util/TruncatingBufferedWriterTest.java    |  63 ++-
 src/changes/changes.xml                            |   3 +
 .../asciidoc/manual/json-template-layout.adoc.vm   |  10 +-
 10 files changed, 985 insertions(+), 73 deletions(-)

diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java
index e2a538a7a5..5c31e00b33 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ExceptionResolver.java
@@ -71,8 +71,13 @@ import java.util.regex.PatternSyntaxException;
  * first.
  * <p>
  * If a stringified stack trace truncation takes place, it will be indicated
- * with <tt>suffix</tt>, which by default is set to the configured
+ * with a <tt>suffix</tt>, which by default is set to the configured
  * <tt>truncatedStringSuffix</tt> in the layout, unless explicitly provided.
+ * Every truncation suffix is prefixed with a newline.
+ * <p>
+ * Stringified stack trace truncation operates in <tt>Caused by:</tt> and
+ * <tt>Suppressed:</tt> label blocks. That is, matchers are executed against
+ * each label in isolation.
  * <p>
  * <tt>elementTemplate</tt> is an object describing the template to be used
  * while resolving the {@link StackTraceElement} array. If <tt>stringified</tt>
@@ -138,7 +143,7 @@ import java.util.regex.PatternSyntaxException;
  *   "stackTrace": {
  *     "stringified": {
  *       "truncation": {
- *         "suffix": ">",
+ *         "suffix": "... [truncated]",
  *         "pointMatcherStrings": ["at javax.servlet.http.HttpServlet.service"]
  *       }
  *     }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
index 0dee8d08e8..4e72d1a718 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
@@ -16,9 +16,7 @@
  */
 package org.apache.logging.log4j.layout.template.json.resolver;
 
-import org.apache.logging.log4j.layout.template.json.util.TruncatingBufferedPrintWriter;
-import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.layout.template.json.util.Recycler;
+import org.apache.logging.log4j.layout.template.json.util.*;
 
 import java.util.List;
 import java.util.function.Supplier;
@@ -31,7 +29,11 @@ import java.util.stream.Collectors;
  */
 final class StackTraceStringResolver implements StackTraceResolver {
 
-    private final Recycler<TruncatingBufferedPrintWriter> writerRecycler;
+    private final Recycler<TruncatingBufferedPrintWriter> srcWriterRecycler;
+
+    private final Recycler<TruncatingBufferedPrintWriter> dstWriterRecycler;
+
+    private final Recycler<CharSequencePointer> sequencePointerRecycler;
 
     private final boolean truncationEnabled;
 
@@ -49,9 +51,15 @@ final class StackTraceStringResolver implements StackTraceResolver {
         final Supplier<TruncatingBufferedPrintWriter> writerSupplier =
                 () -> TruncatingBufferedPrintWriter.ofCapacity(
                         context.getMaxStringByteCount());
-        this.writerRecycler = context
-                .getRecyclerFactory()
-                .create(writerSupplier, TruncatingBufferedPrintWriter::close);
+        final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
+        this.srcWriterRecycler =
+                recyclerFactory.create(
+                        writerSupplier, TruncatingBufferedPrintWriter::close);
+        this.dstWriterRecycler =
+                recyclerFactory.create(
+                        writerSupplier, TruncatingBufferedPrintWriter::close);
+        this.sequencePointerRecycler =
+                recyclerFactory.create(CharSequencePointer::new);
         this.truncationEnabled =
                 !truncationPointMatcherStrings.isEmpty() ||
                         !truncationPointMatcherRegexes.isEmpty();
@@ -75,56 +83,236 @@ final class StackTraceStringResolver implements StackTraceResolver {
     public void resolve(
             final Throwable throwable,
             final JsonWriter jsonWriter) {
-        final TruncatingBufferedPrintWriter writer = writerRecycler.acquire();
+        final TruncatingBufferedPrintWriter srcWriter = srcWriterRecycler.acquire();
         try {
-            throwable.printStackTrace(writer);
-            truncate(writer);
-            jsonWriter.writeString(writer.buffer(), 0, writer.position());
+            throwable.printStackTrace(srcWriter);
+            final TruncatingBufferedPrintWriter dstWriter = truncate(srcWriter);
+            jsonWriter.writeString(dstWriter);
         } finally {
-            writerRecycler.release(writer);
+            srcWriterRecycler.release(srcWriter);
         }
     }
 
-    private void truncate(final TruncatingBufferedPrintWriter writer) {
+    private TruncatingBufferedPrintWriter truncate(
+            final TruncatingBufferedPrintWriter srcWriter) {
 
         // Short-circuit if truncation is not enabled.
         if (!truncationEnabled) {
-            return;
+            return srcWriter;
+        }
+
+        // Allocate temporary buffers and truncate the input.
+        final TruncatingBufferedPrintWriter dstWriter =
+                dstWriterRecycler.acquire();
+        try {
+            final CharSequencePointer sequencePointer =
+                    sequencePointerRecycler.acquire();
+            try {
+                truncate(srcWriter, dstWriter, sequencePointer);
+            } finally {
+                sequencePointerRecycler.release(sequencePointer);
+            }
+        } finally {
+            dstWriterRecycler.release(dstWriter);
+        }
+        return dstWriter;
+
+    }
+
+    private void truncate(
+            final TruncatingBufferedPrintWriter srcWriter,
+            final TruncatingBufferedPrintWriter dstWriter,
+            final CharSequencePointer sequencePointer) {
+        int startIndex = 0;
+        for (;;) {
+
+            // Find the next label start, if present.
+            final int labeledLineStartIndex =
+                    findLabeledLineStartIndex(
+                            srcWriter, startIndex, srcWriter.length());
+            final int endIndex = labeledLineStartIndex >= 0
+                    ? labeledLineStartIndex
+                    : srcWriter.length();
+
+            // Copy up to the truncation point, if it matches.
+            final int truncationPointIndex = findTruncationPointIndex(
+                    srcWriter, startIndex, endIndex, sequencePointer);
+            if (truncationPointIndex > 0) {
+                dstWriter.append(srcWriter, startIndex, truncationPointIndex);
+                dstWriter.append(System.lineSeparator());
+                dstWriter.append(truncationSuffix);
+            }
+
+            // Otherwise, copy the entire labeled block.
+            else {
+                dstWriter.append(srcWriter, startIndex, endIndex);
+            }
+
+            // Copy the label to avoid stepping over it again.
+            if (labeledLineStartIndex > 0) {
+                dstWriter.append(System.lineSeparator());
+                startIndex = labeledLineStartIndex;
+                for (;;) {
+                    final char c = srcWriter.charAt(startIndex++);
+                    dstWriter.append(c);
+                    if (c == ':') {
+                        break;
+                    }
+                }
+            }
+
+            // Otherwise, the source is exhausted, stop.
+            else {
+                break;
+            }
+
         }
+    }
+
+    private int findTruncationPointIndex(
+            final TruncatingBufferedPrintWriter writer,
+            final int startIndex,
+            final int endIndex,
+            final CharSequencePointer sequencePointer) {
 
         // Check for string matches.
         // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
         for (int i = 0; i < truncationPointMatcherStrings.size(); i++) {
             final String matcher = truncationPointMatcherStrings.get(i);
-            final int matchIndex = writer.indexOf(matcher);
+            final int matchIndex = findMatchingIndex(
+                    matcher, writer, startIndex, endIndex);
             if (matchIndex > 0) {
-                final int truncationPointIndex = matchIndex + matcher.length();
-                truncate(writer, truncationPointIndex);
-                return;
+                // No need for `Math.addExact()`, since we have a match:
+                return matchIndex + matcher.length();
             }
         }
 
         // Check for regex matches.
+        CharSequence sequence;
+        if (startIndex == 0 && endIndex == writer.length()) {
+            sequence = writer;
+        } else {
+            sequencePointer.reset(writer, startIndex, writer.length());
+            sequence = sequencePointer;
+        }
         // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
         for (int i = 0; i < groupedTruncationPointMatcherRegexes.size(); i++) {
             final Pattern pattern = groupedTruncationPointMatcherRegexes.get(i);
-            final Matcher matcher = pattern.matcher(writer);
+            final Matcher matcher = pattern.matcher(sequence);
             final boolean matched = matcher.matches();
             if (matched) {
                 final int lastGroup = matcher.groupCount();
-                final int truncationPointIndex = matcher.start(lastGroup);
-                truncate(writer, truncationPointIndex);
-                return;
+                return matcher.start(lastGroup);
             }
         }
 
+        // No matches.
+        return -1;
+
     }
 
-    private void truncate(
-            final TruncatingBufferedPrintWriter writer,
-            final int index) {
-        writer.position(index);
-        writer.print(truncationSuffix);
+    private static int findLabeledLineStartIndex(
+            final CharSequence buffer,
+            final int startIndex,
+            final int endIndex) {
+        // Note that the index arithmetic in this method is not guarded.
+        // That is, there are no `Math.addExact()` or `Math.subtractExact()` usages.
+        // Since we know a priori that we are already operating within buffer limits.
+        for (int bufferIndex = startIndex; bufferIndex < endIndex;) {
+
+            // Find the next line start, if exists.
+            final int lineStartIndex = findLineStartIndex(buffer, bufferIndex, endIndex);
+            if (lineStartIndex < 0) {
+                break;
+            }
+            bufferIndex = lineStartIndex;
+
+            // Skip tabs.
+            while (bufferIndex < endIndex && '\t' == buffer.charAt(bufferIndex)) {
+                bufferIndex++;
+            }
+
+            // Search for the `Caused by: ` occurrence.
+            if (bufferIndex < (endIndex - 11) &&
+                    buffer.charAt(bufferIndex) == 'C' &&
+                    buffer.charAt(bufferIndex + 1) == 'a' &&
+                    buffer.charAt(bufferIndex + 2) == 'u' &&
+                    buffer.charAt(bufferIndex + 3) == 's' &&
+                    buffer.charAt(bufferIndex + 4) == 'e' &&
+                    buffer.charAt(bufferIndex + 5) == 'd' &&
+                    buffer.charAt(bufferIndex + 6) == ' ' &&
+                    buffer.charAt(bufferIndex + 7) == 'b' &&
+                    buffer.charAt(bufferIndex + 8) == 'y' &&
+                    buffer.charAt(bufferIndex + 9) == ':' &&
+                    buffer.charAt(bufferIndex + 10) == ' ') {
+                return lineStartIndex;
+            }
+
+            // Search for the `Suppressed: ` occurrence.
+            else if (bufferIndex < (endIndex - 12) &&
+                    buffer.charAt(bufferIndex) == 'S' &&
+                    buffer.charAt(bufferIndex + 1) == 'u' &&
+                    buffer.charAt(bufferIndex + 2) == 'p' &&
+                    buffer.charAt(bufferIndex + 3) == 'p' &&
+                    buffer.charAt(bufferIndex + 4) == 'r' &&
+                    buffer.charAt(bufferIndex + 5) == 'e' &&
+                    buffer.charAt(bufferIndex + 6) == 's' &&
+                    buffer.charAt(bufferIndex + 7) == 's' &&
+                    buffer.charAt(bufferIndex + 8) == 'e' &&
+                    buffer.charAt(bufferIndex + 9) == 'd' &&
+                    buffer.charAt(bufferIndex + 10) == ':' &&
+                    buffer.charAt(bufferIndex + 11) == ' ') {
+                return lineStartIndex;
+            }
+
+        }
+        return -1;
+    }
+
+    private static int findLineStartIndex(
+            final CharSequence buffer,
+            final int startIndex,
+            final int endIndex) {
+        char prevChar = '-';
+        for (int i = startIndex; i <= endIndex; i++) {
+            if (prevChar == '\n') {
+                return i;
+            }
+            prevChar = buffer.charAt(i);
+        }
+        return -1;
+    }
+
+    private static int findMatchingIndex(
+            final CharSequence matcher,
+            final CharSequence buffer,
+            final int bufferStartIndex,
+            final int bufferEndIndex) {
+
+        // Note that the index arithmetic in this method is not guarded.
+        // That is, there are no `Math.addExact()` or `Math.subtractExact()` usages.
+        // Since we know a priori that we are already operating within buffer limits.
+
+        // While searching for an input of length `n`, no need to traverse the last `n-1` characters.
+        final int effectiveBufferEndIndex = bufferEndIndex - matcher.length() + 1;
+
+        // Perform the search.
+        for (int bufferIndex = bufferStartIndex; bufferIndex <= effectiveBufferEndIndex; bufferIndex++) {
+            boolean found = true;
+            for (int matcherIndex = 0; matcherIndex < matcher.length(); matcherIndex++) {
+                final char matcherChar = matcher.charAt(matcherIndex);
+                final char bufferChar = buffer.charAt(bufferIndex + matcherIndex);
+                if (matcherChar != bufferChar) {
+                    found = false;
+                    break;
+                }
+            }
+            if (found) {
+                return bufferIndex;
+            }
+        }
+        return -1;
+
     }
 
 }
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/CharSequencePointer.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/CharSequencePointer.java
new file mode 100644
index 0000000000..0495e7c7c4
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/CharSequencePointer.java
@@ -0,0 +1,106 @@
+/*
+ * 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.layout.template.json.util;
+
+import java.util.Objects;
+import java.util.stream.IntStream;
+
+/**
+ * A {@link CharSequence} wrapper that allows mutation of the pointed delegate sequence.
+ */
+public final class CharSequencePointer implements CharSequence {
+
+    private CharSequence delegate;
+
+    private int startIndex;
+
+    private int length = -1;
+
+    public void reset(
+            final CharSequence delegate,
+            final int startIndex,
+            final int endIndex) {
+
+        // Check & set the delegate.
+        Objects.requireNonNull(delegate, "delegate");
+        this.delegate = delegate;
+
+        // Check & set the start.
+        if (startIndex < 0) {
+            throw new IndexOutOfBoundsException("invalid start: " + startIndex);
+        }
+
+        // Check & set length.
+        if (endIndex > delegate.length()) {
+            throw new IndexOutOfBoundsException("invalid end: " + endIndex);
+        }
+        this.length = Math.subtractExact(endIndex, startIndex);
+        if (length < 0) {
+            throw new IndexOutOfBoundsException("invalid length: " + length);
+        }
+
+        // Set fields.
+        this.delegate = delegate;
+        this.startIndex = startIndex;
+
+    }
+
+    @Override
+    public int length() {
+        requireReset();
+        return length;
+    }
+
+    @Override
+    public char charAt(final int startIndex) {
+        requireReset();
+        final int delegateStartIndex = Math.addExact(this.startIndex, startIndex);
+        return delegate.charAt(delegateStartIndex);
+    }
+
+    @Override
+    public CharSequence subSequence(final int startIndex, final int endIndex) {
+        throw new UnsupportedOperationException(
+                "operation requires allocation, contradicting with the purpose of the class");
+    }
+
+    @Override
+    public IntStream chars() {
+        throw new UnsupportedOperationException(
+                "operation requires allocation, contradicting with the purpose of the class");
+    }
+
+    @Override
+    public IntStream codePoints() {
+        throw new UnsupportedOperationException(
+                "operation requires allocation, contradicting with the purpose of the class");
+    }
+
+    @Override
+    public String toString() {
+        requireReset();
+        final int endIndex = Math.addExact(startIndex, length);
+        return delegate.toString().substring(startIndex, endIndex);
+    }
+
+    private void requireReset() {
+        if (length < 0) {
+            throw new IllegalStateException("pointer must be reset first");
+        }
+    }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedPrintWriter.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedPrintWriter.java
index 7e9aa3cc82..7f30ab372d 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedPrintWriter.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedPrintWriter.java
@@ -59,11 +59,6 @@ public final class TruncatingBufferedPrintWriter
         return writer.truncated();
     }
 
-    public int indexOf(final CharSequence seq) {
-        Objects.requireNonNull(seq, "seq");
-        return writer.indexOf(seq);
-    }
-
     @Override
     public int length() {
         return writer.length();
@@ -74,6 +69,18 @@ public final class TruncatingBufferedPrintWriter
         return writer.charAt(index);
     }
 
+    @Override
+    public PrintWriter append(final CharSequence seq) {
+        writer.append(seq);
+        return this;
+    }
+
+    @Override
+    public PrintWriter append(final CharSequence seq, final int startIndex, final int endIndex) {
+        writer.append(seq, startIndex, endIndex);
+        return this;
+    }
+
     @Override
     public CharSequence subSequence(final int startIndex, final int endIndex) {
         return writer.subSequence(startIndex, endIndex);
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriter.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriter.java
index 1b88f121d4..62757a3f37 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriter.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriter.java
@@ -18,6 +18,7 @@ package org.apache.logging.log4j.layout.template.json.util;
 
 import java.io.Writer;
 import java.util.Objects;
+import java.util.stream.IntStream;
 
 final class TruncatingBufferedWriter extends Writer implements CharSequence {
 
@@ -203,41 +204,9 @@ final class TruncatingBufferedWriter extends Writer implements CharSequence {
 
     }
 
-    int indexOf(final CharSequence seq) {
-
-        // Short-circuit if there is nothing to match.
-        final int seqLength = seq.length();
-        if (seqLength == 0) {
-            return 0;
-        }
-
-        // Short-circuit if the given input is longer than the buffer.
-        if (seqLength > position) {
-            return -1;
-        }
-
-        // Perform the search.
-        for (int bufferIndex = 0; bufferIndex < position; bufferIndex++) {
-            boolean found = true;
-            for (int seqIndex = 0; seqIndex < seqLength; seqIndex++) {
-                final char s = seq.charAt(seqIndex);
-                final char b = buffer[bufferIndex + seqIndex];
-                if (s != b) {
-                    found = false;
-                    break;
-                }
-            }
-            if (found) {
-                return bufferIndex;
-            }
-        }
-        return -1;
-
-    }
-
     @Override
     public int length() {
-        return position + 1;
+        return position;
     }
 
     @Override
@@ -247,7 +216,20 @@ final class TruncatingBufferedWriter extends Writer implements CharSequence {
 
     @Override
     public String subSequence(final int startIndex, final int endIndex) {
-        return new String(buffer, startIndex, endIndex - startIndex);
+        throw new UnsupportedOperationException(
+                "operation requires allocation, contradicting with the purpose of the class");
+    }
+
+    @Override
+    public IntStream chars() {
+        throw new UnsupportedOperationException(
+                "operation requires allocation, contradicting with the purpose of the class");
+    }
+
+    @Override
+    public IntStream codePoints() {
+        throw new UnsupportedOperationException(
+                "operation requires allocation, contradicting with the purpose of the class");
     }
 
     @Override
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java
new file mode 100644
index 0000000000..c1aa485e21
--- /dev/null
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java
@@ -0,0 +1,437 @@
+/*
+ * 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.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.assertj.core.api.AbstractStringAssert;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.math.BigDecimal;
+import java.net.ServerSocket;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class StackTraceStringResolverTest {
+
+    ////////////////////////////////////////////////////////////////////////////
+    // exceptions //////////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    // Below we create arbitrary exceptions containing stack entries from non-Log4j packages.
+    // Non-Log4j package origin is needed to avoid the truncation (e.g., `... 58 more`) done by `Throwable#printStackTrace()`.
+
+    private static final String EXCEPTION_REGEX_FLAGS = "(?m)(?s)";     // MULTILINE | DOTALL
+
+    private static final String TRUNCATION_SUFFIX = "<truncated>";
+
+    @SuppressWarnings({"BigDecimalMethodWithoutRoundingCalled", "ResultOfMethodCallIgnored"})
+    private static Throwable exception1() {
+        return catchException(() -> BigDecimal.ONE.divide(BigDecimal.ZERO));
+    }
+
+    private static String exception1Regex(final boolean truncated) {
+        final String truncationCorrectionRegex = truncationSuffixRegexOr(truncated, ".divide\\(");
+        return "java.lang.ArithmeticException: Division by zero\r?\n" +
+                "\t+at java.math.BigDecimal" + truncationCorrectionRegex + ".*";
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    private static Throwable exception2() {
+        return catchException(() -> Collections.emptyList().add(0));
+    }
+
+    private static String exception2Regex(final boolean truncated) {
+        final String truncationCorrectionRegex = truncationSuffixRegexOr(truncated, ".add\\(");
+        return "java.lang.UnsupportedOperationException\r?\n" +
+                "\t+at java.util.AbstractList" + truncationCorrectionRegex + ".*";
+    }
+
+    private static Throwable exception3() {
+        return catchException(() -> new ServerSocket(-1));
+    }
+
+    private static String exception3Regex(final boolean truncated) {
+        final String truncationCorrectionRegex = truncationSuffixRegexOr(truncated, ".<init>");
+        return "java.lang.IllegalArgumentException: Port value out of range: -1\r?\n" +
+                "\t+at java.net.ServerSocket" + truncationCorrectionRegex + ".*";
+    }
+
+    private static String truncationSuffixRegexOr(final boolean truncated, final String fallback) {
+        return truncated
+                ? ("\r?\n" + TRUNCATION_SUFFIX)
+                : fallback;
+    }
+
+    private static Throwable catchException(ThrowingRunnable runnable) {
+        try {
+            runnable.run();
+            throw new AssertionError("should not have reached here");
+        } catch (Throwable error) {
+            return error;
+        }
+    }
+
+    @FunctionalInterface
+    private interface ThrowingRunnable {
+
+        void run() throws Throwable;
+
+    }
+
+    @Test
+    void exception1_regex_should_match() {
+        final Throwable error = exception1();
+        final String stackTrace = stackTrace(error);
+        final String regex = exception1Regex(false);
+        Assertions
+                .assertThat(stackTrace)
+                .matches(EXCEPTION_REGEX_FLAGS + regex);
+    }
+
+    @Test
+    void exception2_regex_should_match() {
+        final Throwable error = exception2();
+        final String stackTrace = stackTrace(error);
+        final String regex = exception2Regex(false);
+        Assertions
+                .assertThat(stackTrace)
+                .matches(EXCEPTION_REGEX_FLAGS + regex);
+    }
+
+    @Test
+    void exception3_regex_should_match() {
+        final Throwable error = exception3();
+        final String stackTrace = stackTrace(error);
+        final String regex = exception3Regex(false);
+        Assertions
+                .assertThat(stackTrace)
+                .matches(EXCEPTION_REGEX_FLAGS + regex);
+    }
+
+    private static String stackTrace(final Throwable throwable) {
+        final String encoding = "UTF-8";
+        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+             PrintStream printStream = new PrintStream(outputStream, false, encoding)) {
+            throwable.printStackTrace(printStream);
+            printStream.flush();
+            return outputStream.toString(encoding);
+        } catch (Exception error) {
+            throw new RuntimeException(error);
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // abstract tests //////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    private static abstract class AbstractTestCases {
+
+        private final boolean truncated;
+
+        AbstractTestCases(boolean truncated) {
+            this.truncated = truncated;
+        }
+
+        private String exception1Regex() {
+            return StackTraceStringResolverTest.exception1Regex(truncated);
+        }
+
+        private String exception2Regex() {
+            return StackTraceStringResolverTest.exception2Regex(truncated);
+        }
+
+        private String exception3Regex() {
+            return StackTraceStringResolverTest.exception3Regex(truncated);
+        }
+
+        @Test
+        void exception_should_be_resolved() {
+            final Throwable exception = exception1();
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS + exception1Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+        }
+
+        @Test
+        void exception_with_cause_should_be_resolved() {
+
+            // Create the exception.
+            final Throwable exception = exception1();
+            final Throwable cause = exception2();
+            exception.initCause(cause);
+
+            // Check the serialized exception.
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS +
+                    exception1Regex() +
+                    "\nCaused by: " + exception2Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+
+        }
+
+        @Test
+        void exception_with_causes_should_be_resolved() {
+
+            // Create the exception.
+            final Throwable exception = exception1();
+            final Throwable cause1 = exception2();
+            final Throwable cause2 = exception3();
+            exception.initCause(cause1);
+            cause1.initCause(cause2);
+
+            // Check the serialized exception.
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS +
+                    exception1Regex() +
+                    "\nCaused by: " + exception2Regex() +
+                    "\nCaused by: " + exception3Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+
+        }
+
+        @Test
+        void exception_with_suppressed_should_be_resolved() {
+
+            // Create the exception.
+            final Throwable exception = exception1();
+            final Throwable suppressed = exception2();
+            exception.addSuppressed(suppressed);
+
+            // Check the serialized exception.
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS +
+                    exception1Regex() +
+                    "\n\tSuppressed: " + exception2Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+
+        }
+
+        @Test
+        void exception_with_suppresseds_should_be_resolved() {
+
+            // Create the exception.
+            final Throwable exception = exception1();
+            final Throwable suppressed1 = exception2();
+            final Throwable suppressed2 = exception3();
+            exception.addSuppressed(suppressed1);
+            exception.addSuppressed(suppressed2);
+
+            // Check the serialized exception.
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS +
+                    exception1Regex() +
+                    "\n\tSuppressed: " + exception2Regex() +
+                    "\n\tSuppressed: " + exception3Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+
+        }
+
+        @Test
+        void exception_with_cause_and_suppressed_should_be_resolved() {
+
+            // Create the exception.
+            final Throwable exception = exception1();
+            final Throwable suppressed = exception2();
+            final Throwable cause = exception3();
+            exception.addSuppressed(suppressed);
+            exception.initCause(cause);
+
+            // Check the serialized exception.
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS +
+                    exception1Regex() +
+                    "\n\tSuppressed: " + exception2Regex() +
+                    "\nCaused by: " + exception3Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+
+        }
+
+        @Test
+        void exception_with_cause_with_suppressed_should_be_resolved() {
+
+            // Create the exception.
+            final Throwable exception = exception1();
+            final Throwable cause = exception2();
+            final Throwable suppressed = exception3();
+            exception.initCause(cause);
+            cause.addSuppressed(suppressed);
+
+            // Check the serialized exception.
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS +
+                    exception1Regex() +
+                    "\nCaused by: " + exception2Regex() +
+                    "\n\tSuppressed: " + exception3Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+
+        }
+
+        @Test
+        void exception_with_suppressed_with_cause_should_be_resolved() {
+
+            // Create the exception.
+            final Throwable exception = exception1();
+            final Throwable suppressed = exception2();
+            final Throwable cause = exception3();
+            exception.addSuppressed(suppressed);
+            suppressed.initCause(cause);
+
+            // Check the serialized exception.
+            final String serializedExceptionRegex = EXCEPTION_REGEX_FLAGS +
+                    exception1Regex() +
+                    "\n\tSuppressed: " + exception2Regex() +
+                    "\n\tCaused by: " + exception3Regex();
+            assertSerializedException(exception, serializedExceptionRegex);
+
+        }
+
+        abstract void assertSerializedException(
+                final Throwable exception,
+                final String regex);
+
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // tests without truncation ////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Nested
+    class WithoutTruncation extends AbstractTestCases {
+
+        WithoutTruncation() {
+            super(false);
+        }
+
+        @Override
+        void assertSerializedException(final Throwable exception, final String regex) {
+            assertSerializedExceptionWithoutTruncation(exception, regex);
+        }
+
+    }
+
+    private static void assertSerializedExceptionWithoutTruncation(
+            final Throwable exception,
+            final String regex) {
+
+        // Create the event template.
+        final Map<String, ?> exceptionResolverTemplate = asMap(
+                "$resolver", "exception",
+                "field", "stackTrace",
+                "stackTrace", asMap("stringified", true));
+
+        // Check the serialized event.
+        assertSerializedException(
+                exceptionResolverTemplate,
+                exception,
+                serializedExceptionAssert -> serializedExceptionAssert.matches(regex));
+
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // tests with `truncationPointMatcherStrings` //////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Nested
+    class WithStringTruncation extends AbstractTestCases {
+
+        WithStringTruncation() {
+            super(true);
+        }
+
+        @Override
+        void assertSerializedException(final Throwable exception, final String regex) {
+            assertSerializedExceptionWithStringTruncation(exception, regex);
+        }
+        
+    }
+
+    private static void assertSerializedExceptionWithStringTruncation(
+            final Throwable exception,
+            final String regex) {
+
+        // Create the event template.
+        final List<String> pointMatcherStrings = pointMatcherStrings();
+        final Map<String, ?> exceptionResolverTemplate = asMap(
+                "$resolver", "exception",
+                "field", "stackTrace",
+                "stackTrace", asMap("stringified", asMap(
+                        "truncation", asMap(
+                                "suffix", TRUNCATION_SUFFIX,
+                                "pointMatcherStrings", pointMatcherStrings))));
+
+        // Check the serialized event.
+        assertSerializedException(
+                exceptionResolverTemplate,
+                exception,
+                serializedExceptionAssert -> serializedExceptionAssert.matches(regex));
+
+    }
+
+    private static List<String> pointMatcherStrings() {
+        final Throwable exception1 = exception1();
+        final Throwable exception2 = exception2();
+        final Throwable exception3 = exception3();
+        return Stream
+                .of(exception1, exception2, exception3)
+                .map(exception -> {
+                    final StackTraceElement stackTraceElement = exception.getStackTrace()[0];
+                    final String className = stackTraceElement.getClassName();
+                    return "at " + className;
+                })
+                .collect(Collectors.toList());
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // utilities ///////////////////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    private static void assertSerializedException(
+            final Map<String, ?> exceptionResolverTemplate,
+            final Throwable exception,
+            final Consumer<AbstractStringAssert<?>> serializedExceptionAsserter) {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(asMap("output", exceptionResolverTemplate));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event.
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setThrown(exception)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            AbstractStringAssert<?> serializedExceptionAssert = assertThat(accessor.getString("output"));
+            serializedExceptionAsserter.accept(serializedExceptionAssert);
+        });
+
+    }
+
+}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/CharSequencePointerTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/CharSequencePointerTest.java
new file mode 100644
index 0000000000..b00270a525
--- /dev/null
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/CharSequencePointerTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.layout.template.json.util;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class CharSequencePointerTest {
+
+    private final CharSequencePointer pointer = new CharSequencePointer();
+
+    @Test
+    void length_should_fail_without_reset() {
+        // noinspection ResultOfMethodCallIgnored
+        assertMissingReset(pointer::length);
+    }
+
+    @Test
+    void charAt_should_fail_without_reset() {
+        assertMissingReset(() -> pointer.charAt(0));
+    }
+
+    @Test
+    void toString_should_fail_without_reset() {
+        // noinspection ResultOfMethodCallIgnored
+        assertMissingReset(pointer::toString);
+    }
+
+    private static void assertMissingReset(final Runnable runnable) {
+        Assertions
+                .assertThatThrownBy(runnable::run)
+                .isInstanceOf(IllegalStateException.class)
+                .hasMessage("pointer must be reset first");
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "'',0,0,''",
+            "foo,0,1,f",
+            "foo,1,1,''",
+            "foo,1,2,o",
+            "foo,3,3,''"
+    })
+    void toString_should_subSequence(
+            final CharSequence delegate,
+            final int startIndex,
+            final int endIndex,
+            final String expectedOutput) {
+        pointer.reset(delegate, startIndex, endIndex);
+        Assertions.assertThat(pointer).hasToString(expectedOutput);
+    }
+
+    @Test
+    void subSequence_should_not_be_supported() {
+        pointer.reset("", 0, 0);
+        assertUnsupportedOperation(() -> pointer.subSequence(0, 0));
+    }
+
+    @Test
+    void chars_should_not_be_supported() {
+        pointer.reset("", 0, 0);
+        assertUnsupportedOperation(() -> pointer.subSequence(0, 0));
+    }
+
+    @Test
+    void codePoints_should_not_be_supported() {
+        pointer.reset("", 0, 0);
+        assertUnsupportedOperation(() -> pointer.subSequence(0, 0));
+    }
+
+    private static void assertUnsupportedOperation(final Runnable runnable) {
+        Assertions
+                .assertThatThrownBy(runnable::run)
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessage("operation requires allocation, contradicting with the purpose of the class");
+    }
+
+    @Test
+    void reset_should_fail_on_null_delegate() {
+        Assertions
+                .assertThatThrownBy(() -> pointer.reset(null, 0, 0))
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("delegate");
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "foo,-1,3,invalid start: -1",
+            "foo,4,3,invalid length: -1",
+            "foo,0,-1,invalid length: -1",
+            "foo,1,0,invalid length: -1",
+            "foo,0,4,invalid end: 4"
+    })
+    void reset_should_fail_on_invalid_indices(
+            final CharSequence delegate,
+            final int startIndex,
+            final int endIndex,
+            final String expectedErrorMessage) {
+        Assertions
+                .assertThatThrownBy(() -> pointer.reset(delegate, startIndex, endIndex))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessage(expectedErrorMessage);
+    }
+
+}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriterTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriterTest.java
index b52d453a57..113e8dd5af 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriterTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriterTest.java
@@ -19,6 +19,8 @@ package org.apache.logging.log4j.layout.template.json.util;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.function.Consumer;
+
 class TruncatingBufferedWriterTest {
 
     @Test
@@ -225,7 +227,7 @@ class TruncatingBufferedWriterTest {
         verifyTruncation(writer, 'n');
     }
 
-    private void verifyTruncation(
+    private static void verifyTruncation(
             final TruncatingBufferedWriter writer,
             final char c) {
         Assertions.assertThat(writer.buffer()).isEqualTo(new char[]{c});
@@ -235,10 +237,67 @@ class TruncatingBufferedWriterTest {
         verifyClose(writer);
     }
 
-    private void verifyClose(final TruncatingBufferedWriter writer) {
+    private static void verifyClose(final TruncatingBufferedWriter writer) {
         writer.close();
         Assertions.assertThat(writer.position()).isEqualTo(0);
         Assertions.assertThat(writer.truncated()).isFalse();
     }
 
+    @Test
+    void test_length_and_position() {
+
+        // Create the writer and the verifier.
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(2);
+        final Consumer<Integer> positionAndLengthVerifier =
+                (final Integer expected) -> Assertions
+                        .assertThat(writer.position())
+                        .isEqualTo(writer.length())
+                        .isEqualTo(expected);
+
+        // Check the initial condition.
+        positionAndLengthVerifier.accept(0);
+
+        // Append the 1st character and verify.
+        writer.write("a");
+        positionAndLengthVerifier.accept(1);
+
+        // Append the 2nd character and verify.
+        writer.write("b");
+        positionAndLengthVerifier.accept(2);
+
+        // Append the 3rd to-be-truncated character and verify.
+        writer.write("c");
+        positionAndLengthVerifier.accept(2);
+
+        // Reposition the writer and verify.
+        writer.position(1);
+        positionAndLengthVerifier.accept(1);
+
+    }
+
+    @Test
+    void subSequence_should_not_be_supported() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(2);
+        assertUnsupportedOperation(() -> writer.subSequence(0, 0));
+    }
+
+    @Test
+    void chars_should_not_be_supported() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(2);
+        assertUnsupportedOperation(() -> writer.subSequence(0, 0));
+    }
+
+    @Test
+    void codePoints_should_not_be_supported() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(2);
+        assertUnsupportedOperation(() -> writer.subSequence(0, 0));
+    }
+
+    private static void assertUnsupportedOperation(final Runnable runnable) {
+        Assertions
+                .assertThatThrownBy(runnable::run)
+                .isInstanceOf(UnsupportedOperationException.class)
+                .hasMessage("operation requires allocation, contradicting with the purpose of the class");
+    }
+
 }
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index de42842d74..056235d53a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -30,6 +30,9 @@
          - "remove" - Removed
     -->
     <release version="2.18.1" date="2022-MM-DD" description="GA Release 2.18.1">
+      <action issue="LOG4J2-3556" dev="vy" type="fix" due-to=" Arthur Gavlyukovskiy">
+        Make JsonTemplateLayout stack trace truncation operate for each label block.
+      </action>
       <action issue="LOG4J2-3550" dev="rgoers" type="fix" due-to="DongjianPeng">
         SystemPropertyAribiter was assigning the value as the name.
       </action>
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc.vm b/src/site/asciidoc/manual/json-template-layout.adoc.vm
index 53410b2c6f..2fbce39a54 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc.vm
+++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm
@@ -702,9 +702,13 @@ are provided.
 stringified stack traces after the given matching point. If both parameters are
 provided, `pointMatcherStrings` will be checked first.
 
-If a stringified stack trace truncation takes place, it will be indicated with
+If a stringified stack trace truncation takes place, it will be indicated with a
 `suffix`, which by default is set to the configured `truncatedStringSuffix` in
-the layout, unless explicitly provided.
+the layout, unless explicitly provided. Every truncation suffix is prefixed with
+a newline.
+
+Stringified stack trace truncation operates in `Caused by:` and `Suppressed:`
+label blocks. That is, matchers are executed against each label in isolation.
 
 `elementTemplate` is an object describing the template to be used while
 resolving the `StackTraceElement` array. If `stringified` is set to `true`,
@@ -783,7 +787,7 @@ truncated after the given point matcher:
   "stackTrace": {
     "stringified": {
       "truncation": {
-        "suffix": ">",
+        "suffix": "... [truncated]",
         "pointMatcherStrings": ["at javax.servlet.http.HttpServlet.service"]
       }
     }


[logging-log4j2] 02/03: LOG4J2-3556 Fix stack trace truncation matcher regex.

Posted by vy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit a0c24a5a398d21e274531cdb8a6eac21b389d669
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Wed Aug 17 11:26:16 2022 +0200

    LOG4J2-3556 Fix stack trace truncation matcher regex.
---
 .../layout/template/json/resolver/StackTraceStringResolver.java     | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
index 4e72d1a718..92c2d33252 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
@@ -74,8 +74,10 @@ final class StackTraceStringResolver implements StackTraceResolver {
         return regexes
                 .stream()
                 .map(regex -> Pattern.compile(
-                        "^.*(" + regex + ")(.*)$",
-                        Pattern.MULTILINE | Pattern.DOTALL))
+                        ".*?" +                 // Make `.*` lazy with `?` suffix, since we want to find the _first_ match of `regex`.
+                                regex +         // Match the user input.
+                                "(.*)",         // Group that is to be truncated.
+                        Pattern.DOTALL))
                 .collect(Collectors.toList());
     }