From 7433b93769c060f9dd054bf455e305ce979c0869 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 6 Feb 2025 13:11:58 -0800 Subject: [PATCH] Add stack trace printer support for structured logging Introduce a new `StackTracePrinter` interface (and a standard implementation) that can be used to print stack traces in a custom form. The existing `StructuredLoggingJsonProperties` have been updated with a nested `StackTrace` record that supports common customization options or allows a custom `StackTracePrinter` to be used. Closes gh-43864 --- .../reference/pages/features/logging.adoc | 51 ++ .../boot/logging/StackTracePrinter.java | 57 +++ .../logging/StandardStackTracePrinter.java | 474 ++++++++++++++++++ ...ticCommonSchemaStructuredLogFormatter.java | 26 +- .../boot/logging/log4j2/Extractor.java | 59 +++ ...tendedLogFormatStructuredLogFormatter.java | 25 +- .../LogstashStructuredLogFormatter.java | 16 +- .../logging/log4j2/StructuredLogLayout.java | 11 +- ...ticCommonSchemaStructuredLogFormatter.java | 14 +- .../boot/logging/logback/Extractor.java | 58 +++ ...tendedLogFormatStructuredLogFormatter.java | 27 +- .../LogstashStructuredLogFormatter.java | 14 +- .../logging/logback/StructuredLogEncoder.java | 14 +- .../structured/StructuredLogFormatter.java | 4 +- .../StructuredLogFormatterFactory.java | 13 +- .../StructuredLoggingJsonProperties.java | 80 ++- ...itional-spring-configuration-metadata.json | 49 ++ .../StandardStackTracePrinterTests.java | 390 ++++++++++++++ .../boot/logging/TestException.java | 82 +++ .../AbstractStructuredLoggingTests.java | 7 +- ...mmonSchemaStructuredLogFormatterTests.java | 28 +- .../boot/logging/log4j2/ExtractorTests.java | 66 +++ ...dLogFormatStructuredLogFormatterTests.java | 31 +- .../LogstashStructuredLogFormatterTests.java | 16 +- .../log4j2/SimpleStackTracePrinter.java | 37 ++ ...sts.java => StructuredLogLayoutTests.java} | 20 +- .../AbstractStructuredLoggingTests.java | 10 +- ...mmonSchemaStructuredLogFormatterTests.java | 31 +- .../boot/logging/logback/ExtractorTests.java | 73 +++ ...dLogFormatStructuredLogFormatterTests.java | 34 +- .../LogstashStructuredLogFormatterTests.java | 17 +- .../logback/SimpleStackTracePrinter.java | 37 ++ .../logback/StructuredLogEncoderTests.java | 20 +- .../StructuredLogFormatterFactoryTests.java | 32 +- ...nPropertiesJsonMembersCustomizerTests.java | 15 +- .../StructuredLoggingJsonPropertiesTests.java | 147 +++++- src/checkstyle/import-control.xml | 1 + 37 files changed, 1957 insertions(+), 129 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StackTracePrinter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StandardStackTracePrinter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Extractor.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/Extractor.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/StandardStackTracePrinterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/TestException.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ExtractorTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SimpleStackTracePrinter.java rename spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/{StructuredLoggingLayoutTests.java => StructuredLogLayoutTests.java} (84%) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ExtractorTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/SimpleStackTracePrinter.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index f2aa01cba62..787e75928ea 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -647,6 +647,57 @@ You can also declare implementations by listing them in a `META-INF/spring.facto +[[features.logging.structured.customizing-stack-traces]] +=== Customizing Structured Logging Stack Traces + +Complete stack traces are included in the JSON output whenever a message is logged with an exception. +This amount of information may be costly to process by your log ingestion system, so you may want to tune the way that stack traces are printed. + +To do this, you can use one or more of the following properties: + +|=== +| Property | Description + +| configprop:logging.structured.json.stacktrace.root[] +| Use `last` to print the root item last (same as Java) or `first` to print the root item first. + +| configprop:logging.structured.json.stacktrace.max-length[] +| The maximum length that should be printed + +| configprop:logging.structured.json.stacktrace.max-throwable-depth[] +| The maximum number of frames to print per stack trace (including common and suppressed frames) + +| configprop:logging.structured.json.stacktrace.include-common-frames[] +| If common frames should be included or removed + +| configprop:logging.structured.json.stacktrace.include-hashes[] +| If a hash of the stack trace should be included +|=== + +For example, the following will use root first stack traces, limit their length, and include hashes. + +[configprops,yaml] +---- +logging: + structured: + json: + stacktrace: + root: first + max-length: 1024 + include-common-frames: true + include-hashes: true +---- + +[TIP] +==== +If you need complete control over stack trace printing you can set configprop:logging.structured.json.stacktrace.printer[] to the name of a javadoc:org.springframework.boot.logging.StackTracePrinter[] implementation. +You can also set it to `logging-system` to force regular logging system stack trace output to be used. + +Your `StackTracePrinter` implementation can also include a constructor argument that accepts a javadoc:org.springframework.boot.logging.StandardStackTracePrinter[] if it wishes to apply further customization to the stack trace printer created from the properties. +==== + + + [[features.logging.structured.other-formats]] === Supporting Other Structured Logging Formats diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StackTracePrinter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StackTracePrinter.java new file mode 100644 index 00000000000..5ca12a33ab2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StackTracePrinter.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.logging; + +import java.io.IOException; +import java.io.UncheckedIOException; + +/** + * Interface that can be used to print the stack trace of a {@link Throwable}. + * + * @author Phillip Webb + * @since 3.5.0 + * @see StandardStackTracePrinter + */ +@FunctionalInterface +public interface StackTracePrinter { + + /** + * Return a {@link String} containing the printed stack trace for a given + * {@link Throwable}. + * @param throwable the throwable that should have its stack trace printed + * @return the stack trace string + */ + default String printStackTraceToString(Throwable throwable) { + try { + StringBuilder out = new StringBuilder(4096); + printStackTrace(throwable, out); + return out.toString(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Prints a stack trace for the given {@link Throwable}. + * @param throwable the throwable that should have its stack trace printed + * @param out the destination to write output + * @throws IOException on IO error + */ + void printStackTrace(Throwable throwable, Appendable out) throws IOException; + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StandardStackTracePrinter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StandardStackTracePrinter.java new file mode 100644 index 00000000000..d35a6797a29 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StandardStackTracePrinter.java @@ -0,0 +1,474 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.logging; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; + +import org.springframework.util.Assert; + +/** + * {@link StackTracePrinter} that prints a standard form stack trace. This printer + * produces a result in a similar form to {@link Throwable#printStackTrace()}, but offers + * more customization options. + * + * @author Phillip Webb + * @since 3.5.0 + */ +public final class StandardStackTracePrinter implements StackTracePrinter { + + private static final String DEFAULT_LINE_SEPARATOR = System.lineSeparator(); + + private static final ToIntFunction DEFAULT_FRAME_HASHER = (frame) -> Objects + .hash(frame.getClassName(), frame.getMethodName(), frame.getLineNumber()); + + private static final int UNLIMTED = Integer.MAX_VALUE; + + private final EnumSet