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/22 19:56:30 UTC

[logging-log4j2] branch master updated: LOG4J2-3556 Make JsonTemplateLayout stack trace truncation operate for each label block. (#1002)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6f1828c0fb LOG4J2-3556 Make JsonTemplateLayout stack trace truncation operate for each label block. (#1002)
6f1828c0fb is described below

commit 6f1828c0fb2d81ffad3b8aa00d8b12ed93b6eddd
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Mon Aug 22 07:34:36 2022 +0200

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

diff --git a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
index ee27d4f425..bd2a2a9f46 100644
--- a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
+++ b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
@@ -82,6 +82,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 {
@@ -541,11 +542,11 @@ class JsonTemplateLayoutTest {
         // Create the log event.
         final SimpleMessage message = new SimpleMessage("Hello, World!");
         final LogEvent logEvent = Log4jLogEvent
-            .newBuilder()
-            .setLoggerName(LOGGER_NAME)
-            .setLevel(Level.INFO)
-            .setMessage(message)
-            .build();
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
 
         // Create the template.
         final String template = writeJson(asMap(
@@ -771,85 +772,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.
@@ -972,8 +905,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)));
 
@@ -1140,161 +1072,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() {
 
@@ -1327,9 +1104,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));
 
     }
 
@@ -1356,9 +1133,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]+"));
 
     }
 
@@ -1424,7 +1201,6 @@ class JsonTemplateLayoutTest {
                 .newBuilder()
                 .setLoggerName(LOGGER_NAME)
                 .setMessage(message)
-                .setThrown(NonAsciiUtf8MethodNameContainingException.INSTANCE)
                 .build();
 
         // Check the serialized event.
@@ -1816,9 +1592,7 @@ class JsonTemplateLayoutTest {
         final String expectedSerializedLogEventJson =
                 "{}" + JsonTemplateLayoutDefaults.getEventDelimiter();
         final String actualSerializedLogEventJson = layout.toSerializable(logEvent);
-        Assertions
-                .assertThat(actualSerializedLogEventJson)
-                .isEqualTo(expectedSerializedLogEventJson);
+        assertThat(actualSerializedLogEventJson).isEqualTo(expectedSerializedLogEventJson);
 
     }
 
@@ -1849,8 +1623,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-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java
new file mode 100644
index 0000000000..dad3486cb9
--- /dev/null
+++ b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolverTest.java
@@ -0,0 +1,658 @@
+/*
+ * 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.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
+import org.assertj.core.api.AbstractStringAssert;
+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.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;
+
+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 = "(?s)";     // 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.base/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.base/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.base/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);
+        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);
+        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);
+        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);
+
+        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);
+            });
+
+        }
+
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // tests without truncation ////////////////////////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Nested
+    class WithoutTruncation extends AbstractTestCases {
+
+        WithoutTruncation() {
+            super(false);
+        }
+
+        @Override
+        void assertSerializedException(final Throwable exception, final String regex) {
+            assertSerializedExceptionWithoutTruncation(exception, regex);
+        }
+
+        private 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.
+            AbstractTestCases.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);
+            });
+
+        }
+
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
+    // tests with `truncationPointMatcherStrings` //////////////////////////////
+    ////////////////////////////////////////////////////////////////////////////
+
+    @Nested
+    class WithTruncation extends AbstractTestCases {
+
+        WithTruncation() {
+            super(true);
+        }
+
+        @Override
+        void assertSerializedException(final Throwable exception, final String regex) {
+            assertSerializedExceptionWithStringTruncation(exception, 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));
+
+        }
+
+        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 moduleName = stackTraceElement.getModuleName();
+            final String className = stackTraceElement.getClassName();
+            return "at " + moduleName + "/" + className;
+        }
+
+        private String pointMatcherRegex(Throwable exception) {
+            String string = pointMatcherString(exception);
+            return matchingRegex(string);
+        }
+
+        /**
+         * @return a regex matching the given input
+         */
+        private String matchingRegex(String string) {
+            return "[" + string.charAt(0) + "]" + Pattern.quote(string.substring(1));
+        }
+        
+    }
+
+    @Test
+    void nonAscii_utf8_method_name_should_get_serialized() {
+
+        // Create the log event.
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setThrown(NonAsciiUtf8MethodNameContainingException.INSTANCE)
+                .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));
+
+    }
+
+    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);
+        }
+
+    }
+
+}
diff --git a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/CharSequencePointerTest.java b/log4j-layout-template-json-test/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-test/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-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriterTest.java b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriterTest.java
index b52d453a57..113e8dd5af 100644
--- a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/TruncatingBufferedWriterTest.java
+++ b/log4j-layout-template-json-test/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/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 3aa2774d11..26020b5eb5 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..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
@@ -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();
@@ -66,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());
     }
 
@@ -75,56 +85,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/src/changes/changes.xml b/src/changes/changes.xml
index 745ecf18a9..f5f1425fc9 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -205,6 +205,9 @@
       <action issue="LOG4J2-3578" dev="rgoers" type="fix">
         Generate new SSL certs for testing.
       </action>
+      <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-3573" dev="vy" type="remove" due-to=" Wolff Bock von Wuelfingen">
         Removed build page in favor of a single build instructions file.
       </action>
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc.vm b/src/site/asciidoc/manual/json-template-layout.adoc.vm
index 26b0922d08..f1bb103a21 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"]
       }
     }