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
This commit is contained in:
Phillip Webb 2025-02-06 13:11:58 -08:00
parent 291e5d8bd3
commit 7433b93769
37 changed files with 1957 additions and 129 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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<StackTraceElement> DEFAULT_FRAME_HASHER = (frame) -> Objects
.hash(frame.getClassName(), frame.getMethodName(), frame.getLineNumber());
private static final int UNLIMTED = Integer.MAX_VALUE;
private final EnumSet<Option> options;
private final int maximumLength;
private final String lineSeparator;
private final Predicate<Throwable> filter;
private final BiPredicate<Integer, StackTraceElement> frameFilter;
private final Function<Throwable, String> formatter;
private final Function<StackTraceElement, String> frameFormatter;
private final ToIntFunction<StackTraceElement> frameHasher;
private StandardStackTracePrinter(EnumSet<Option> options, int maximumLength, String lineSeparator,
Predicate<Throwable> filter, BiPredicate<Integer, StackTraceElement> frameFilter,
Function<Throwable, String> formatter, Function<StackTraceElement, String> frameFormatter,
ToIntFunction<StackTraceElement> frameHasher) {
this.options = options;
this.maximumLength = maximumLength;
this.lineSeparator = (lineSeparator != null) ? lineSeparator : DEFAULT_LINE_SEPARATOR;
this.filter = (filter != null) ? filter : (t) -> true;
this.frameFilter = (frameFilter != null) ? frameFilter : (i, t) -> true;
this.formatter = (formatter != null) ? formatter : Object::toString;
this.frameFormatter = (frameFormatter != null) ? frameFormatter : Object::toString;
this.frameHasher = frameHasher;
}
@Override
public void printStackTrace(Throwable throwable, Appendable out) throws IOException {
if (this.filter.test(throwable)) {
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
Output output = new Output(out);
Print print = new Print("", "", output);
printFullStackTrace(seen, print, new StackTrace(throwable), null);
}
}
private void printFullStackTrace(Set<Throwable> seen, Print print, StackTrace stackTrace, StackTrace enclosing)
throws IOException {
if (stackTrace == null) {
return;
}
if (!seen.add(stackTrace.throwable())) {
String hashPrefix = stackTrace.hashPrefix(this.frameHasher);
String throwable = this.formatter.apply(stackTrace.throwable());
print.circularReference(hashPrefix, throwable);
return;
}
StackTrace cause = stackTrace.cause();
if (!hasOption(Option.ROOT_FIRST)) {
printSingleStackTrace(seen, print, stackTrace, enclosing);
printFullStackTrace(seen, print.withCausedByCaption(cause), cause, stackTrace);
}
else {
printFullStackTrace(seen, print, cause, stackTrace);
printSingleStackTrace(seen, print.withWrappedByCaption(cause), stackTrace, enclosing);
}
}
private void printSingleStackTrace(Set<Throwable> seen, Print print, StackTrace stackTrace, StackTrace enclosing)
throws IOException {
String hashPrefix = stackTrace.hashPrefix(this.frameHasher);
String throwable = this.formatter.apply(stackTrace.throwable());
print.thrown(hashPrefix, throwable);
printFrames(print, stackTrace, enclosing);
if (!hasOption(Option.HIDE_SUPPRESSED)) {
for (StackTrace suppressed : stackTrace.suppressed()) {
printFullStackTrace(seen, print.withSuppressedCaption(), suppressed, stackTrace);
}
}
}
private void printFrames(Print print, StackTrace stackTrace, StackTrace enclosing) throws IOException {
int commonFrames = (!hasOption(Option.SHOW_COMMON_FRAMES)) ? stackTrace.commonFramesCount(enclosing) : 0;
int filteredFrames = 0;
for (int i = 0; i < stackTrace.frames().length - commonFrames; i++) {
StackTraceElement element = stackTrace.frames()[i];
if (!this.frameFilter.test(i, element)) {
filteredFrames++;
continue;
}
print.omittedFilteredFrames(filteredFrames);
filteredFrames = 0;
print.at(this.frameFormatter.apply(element));
}
print.omittedFilteredFrames(filteredFrames);
if (commonFrames != 0) {
print.omittedCommonFrames(commonFrames);
}
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that will print all
* common frames rather the replacing them with the {@literal "... N more"} message.
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withCommonFrames() {
return withOption(Option.SHOW_COMMON_FRAMES);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that will not print
* {@link Throwable#getSuppressed() suppressed} items.
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withoutSuppressed() {
return withOption(Option.HIDE_SUPPRESSED);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that will use ellipses
* to truncate output longer that the specified length.
* @param maximumLength the maximum length that can be printed
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withMaximumLength(int maximumLength) {
Assert.isTrue(maximumLength > 0, "'maximumLength' must be positive");
return new StandardStackTracePrinter(this.options, maximumLength, this.lineSeparator, this.filter,
this.frameFilter, this.formatter, this.frameFormatter, this.frameHasher);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that filter frames
* (including caused and suppressed) deeper then the specified maximum.
* @param maximumThrowableDepth the maximum throwable depth
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withMaximumThrowableDepth(int maximumThrowableDepth) {
Assert.isTrue(maximumThrowableDepth > 0, "'maximumThrowableDepth' must be positive");
return withFrameFilter((index, element) -> index < maximumThrowableDepth);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that will only include
* throwables (excluding caused and suppressed) that match the given predicate.
* @param predicate the predicate used to filter the throwable
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withFilter(Predicate<Throwable> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return new StandardStackTracePrinter(this.options, this.maximumLength, this.lineSeparator,
this.filter.and(predicate), this.frameFilter, this.formatter, this.frameFormatter, this.frameHasher);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that will only include
* frames that match the given predicate.
* @param predicate the predicate used to filter frames
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withFrameFilter(BiPredicate<Integer, StackTraceElement> predicate) {
Assert.notNull(predicate, "'predicate' must not be null");
return new StandardStackTracePrinter(this.options, this.maximumLength, this.lineSeparator, this.filter,
this.frameFilter.and(predicate), this.formatter, this.frameFormatter, this.frameHasher);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that print the stack
* trace using the specified line separator.
* @param lineSeparator the line separator to use
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withLineSeparator(String lineSeparator) {
Assert.notNull(lineSeparator, "'lineSeparator' must not be null");
return new StandardStackTracePrinter(this.options, this.maximumLength, lineSeparator, this.filter,
this.frameFilter, this.formatter, this.frameFormatter, this.frameHasher);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one uses the specified
* formatter to create a string representation of a throwable.
* @param formatter the formatter to use
* @return a new {@link StandardStackTracePrinter} instance
* @see #withLineSeparator(String)
*/
public StandardStackTracePrinter withFormatter(Function<Throwable, String> formatter) {
Assert.notNull(formatter, "'formatter' must not be null");
return new StandardStackTracePrinter(this.options, this.maximumLength, this.lineSeparator, this.filter,
this.frameFilter, formatter, this.frameFormatter, this.frameHasher);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one uses the specified
* formatter to create a string representation of a frame.
* @param frameFormatter the frame formatter to use
* @return a new {@link StandardStackTracePrinter} instance
* @see #withLineSeparator(String)
*/
public StandardStackTracePrinter withFrameFormatter(Function<StackTraceElement, String> frameFormatter) {
Assert.notNull(frameFormatter, "'frameFormatter' must not be null");
return new StandardStackTracePrinter(this.options, this.maximumLength, this.lineSeparator, this.filter,
this.frameFilter, this.formatter, frameFormatter, this.frameHasher);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that generates and
* prints hashes for each stacktrace.
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withHashes() {
return withHashes(true);
}
/**
* Return a new {@link StandardStackTracePrinter} from this one that changes if hashes
* should be generated and printed for each stacktrace.
* @param hashes if hashes should be added
* @return a new {@link StandardStackTracePrinter} instance
*/
public StandardStackTracePrinter withHashes(boolean hashes) {
return withHashes((!hashes) ? null : DEFAULT_FRAME_HASHER);
}
public StandardStackTracePrinter withHashes(ToIntFunction<StackTraceElement> frameHasher) {
return new StandardStackTracePrinter(this.options, this.maximumLength, this.lineSeparator, this.filter,
this.frameFilter, this.formatter, this.frameFormatter, frameHasher);
}
private StandardStackTracePrinter withOption(Option option) {
EnumSet<Option> options = EnumSet.copyOf(this.options);
options.add(option);
return new StandardStackTracePrinter(options, this.maximumLength, this.lineSeparator, this.filter,
this.frameFilter, this.formatter, this.frameFormatter, this.frameHasher);
}
private boolean hasOption(Option option) {
return this.options.contains(option);
}
/**
* Return a {@link StandardStackTracePrinter} that prints the stack trace with the
* root exception last (the same as {@link Throwable#printStackTrace()}).
* @return a {@link StandardStackTracePrinter} that prints the stack trace root last
*/
public static StandardStackTracePrinter rootLast() {
return new StandardStackTracePrinter(EnumSet.noneOf(Option.class), UNLIMTED, null, null, null, null, null,
null);
}
/**
* Return a {@link StandardStackTracePrinter} that prints the stack trace with the
* root exception first (the opposite of {@link Throwable#printStackTrace()}).
* @return a {@link StandardStackTracePrinter} that prints the stack trace root first
*/
public static StandardStackTracePrinter rootFirst() {
return new StandardStackTracePrinter(EnumSet.of(Option.ROOT_FIRST), UNLIMTED, null, null, null, null, null,
null);
}
/**
* Options supported by this printer.
*/
private enum Option {
ROOT_FIRST, SHOW_COMMON_FRAMES, HIDE_SUPPRESSED
}
/**
* Prints the actual line output.
*/
private record Print(String indent, String caption, Output output) {
void circularReference(String hashPrefix, String throwable) throws IOException {
this.output.println(this.indent, this.caption + "[CIRCULAR REFERENCE: " + hashPrefix + throwable + "]");
}
void thrown(String hashPrefix, String throwable) throws IOException {
this.output.println(this.indent, this.caption + hashPrefix + throwable);
}
void at(String frame) throws IOException {
this.output.println(this.indent, "\tat " + frame);
}
void omittedFilteredFrames(int filteredFrameCount) throws IOException {
if (filteredFrameCount > 0) {
this.output.println(this.indent, "\t... " + filteredFrameCount + " filtered");
}
}
void omittedCommonFrames(int commonFrameCount) throws IOException {
this.output.println(this.indent, "\t... " + commonFrameCount + " more");
}
Print withCausedByCaption(StackTrace causedBy) {
return withCaption(causedBy != null, "", "Caused by: ");
}
Print withWrappedByCaption(StackTrace wrappedBy) {
return withCaption(wrappedBy != null, "", "Wrapped by: ");
}
public Print withSuppressedCaption() {
return withCaption(true, "\t", "Suppressed: ");
}
private Print withCaption(boolean test, String extraIndent, String caption) {
return (test) ? new Print(this.indent + extraIndent, caption, this.output) : this;
}
}
/**
* Line-by-line output.
*/
private class Output {
private static final String ELLIPSIS = "...";
private final Appendable out;
private int remaining;
Output(Appendable out) {
this.out = out;
this.remaining = StandardStackTracePrinter.this.maximumLength - ELLIPSIS.length();
}
void println(String indent, String string) throws IOException {
if (this.remaining > 0) {
String line = indent + string + StandardStackTracePrinter.this.lineSeparator;
if (line.length() > this.remaining) {
line = line.substring(0, this.remaining) + ELLIPSIS;
}
this.out.append(line);
this.remaining -= line.length();
}
}
}
/**
* Holds the stacktrace for a specific throwable and caches things that are expensive
* to calcualte.
*/
private static final class StackTrace {
private final Throwable throwable;
private final StackTraceElement[] frames;
private StackTrace[] suppressed;
private StackTrace cause;
private Integer hash;
private String hashPrefix;
private StackTrace(Throwable throwable) {
this.throwable = throwable;
this.frames = (throwable != null) ? throwable.getStackTrace() : null;
}
Throwable throwable() {
return this.throwable;
}
StackTraceElement[] frames() {
return this.frames;
}
int commonFramesCount(StackTrace other) {
if (other == null) {
return 0;
}
int index = this.frames.length - 1;
int otherIndex = other.frames.length - 1;
while (index >= 0 && otherIndex >= 0 && this.frames[index].equals(other.frames[otherIndex])) {
index--;
otherIndex--;
}
return this.frames.length - 1 - index;
}
StackTrace[] suppressed() {
if (this.suppressed == null && this.throwable != null) {
this.suppressed = Arrays.stream(this.throwable.getSuppressed())
.map(StackTrace::new)
.toArray(StackTrace[]::new);
}
return this.suppressed;
}
StackTrace cause() {
if (this.cause == null && this.throwable != null) {
Throwable cause = this.throwable.getCause();
this.cause = (cause != null) ? new StackTrace(cause) : null;
}
return this.cause;
}
String hashPrefix(ToIntFunction<StackTraceElement> frameHasher) {
if (frameHasher == null || throwable() == null) {
return "";
}
this.hashPrefix = (this.hashPrefix != null) ? this.hashPrefix
: String.format("<#%08x> ", hash(new HashSet<>(), frameHasher));
return this.hashPrefix;
}
private int hash(HashSet<Throwable> seen, ToIntFunction<StackTraceElement> frameHasher) {
if (this.hash != null) {
return this.hash;
}
int hash = 0;
if (cause() != null && seen.add(cause().throwable())) {
hash = cause().hash(seen, frameHasher);
}
hash = 31 * hash + throwable().getClass().getName().hashCode();
for (StackTraceElement frame : frames()) {
hash = 31 * hash + frameHasher.applyAsInt(frame);
}
this.hash = hash;
return hash;
}
}
}

View File

@ -28,6 +28,8 @@ import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
@ -45,12 +47,14 @@ import org.springframework.util.ObjectUtils;
*/
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
ElasticCommonSchemaStructuredLogFormatter(Environment environment,
ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, members), customizer);
super((members) -> jsonMembers(environment, stackTracePrinter, members), customizer);
}
private static void jsonMembers(Environment environment, JsonWriter.Members<LogEvent> members) {
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
JsonWriter.Members<LogEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter);
members.add("@timestamp", LogEvent::getInstant).as(ElasticCommonSchemaStructuredLogFormatter::asTimestamp);
members.add("log.level", LogEvent::getLevel).as(Level::name);
members.add("process.pid", environment.getProperty("spring.application.pid", Long.class))
@ -62,13 +66,9 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
members.from(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));
members.from(LogEvent::getThrownProxy).whenNotNull().usingMembers((thrownProxyMembers) -> {
thrownProxyMembers.add("error.type", ThrowableProxy::getThrowable)
members.from(LogEvent::getThrownProxy)
.whenNotNull()
.as(ObjectUtils::nullSafeClassName);
thrownProxyMembers.add("error.message", ThrowableProxy::getMessage);
thrownProxyMembers.add("error.stack_trace", ThrowableProxy::getExtendedStackTraceAsString);
});
.usingMembers((thrownProxyMembers) -> throwableMembers(thrownProxyMembers, extractor));
members.add("tags", LogEvent::getMarker)
.whenNotNull()
.as(ElasticCommonSchemaStructuredLogFormatter::getMarkers)
@ -80,6 +80,12 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
return java.time.Instant.ofEpochMilli(instant.getEpochMillisecond()).plusNanos(instant.getNanoOfMillisecond());
}
private static void throwableMembers(Members<ThrowableProxy> members, Extractor extractor) {
members.add("error.type", ThrowableProxy::getThrowable).whenNotNull().as(ObjectUtils::nullSafeClassName);
members.add("error.message", ThrowableProxy::getMessage);
members.add("error.stack_trace", extractor::stackTrace);
}
private static Set<String> getMarkers(Marker marker) {
Set<String> result = new TreeSet<>();
addMarkers(result, marker);

View File

@ -0,0 +1,59 @@
/*
* 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.log4j2;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.slf4j.event.LoggingEvent;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.util.Assert;
/**
* Functions to extract items from {@link LoggingEvent}.
*
* @author Phillip Webb
*/
class Extractor {
private final StackTracePrinter stackTracePrinter;
Extractor(StackTracePrinter stackTracePrinter) {
this.stackTracePrinter = stackTracePrinter;
}
String messageAndStackTrace(LogEvent event) {
return event.getMessage().getFormattedMessage() + "\n\n" + stackTrace(event);
}
String stackTrace(LogEvent event) {
return stackTrace(event.getThrownProxy());
}
String stackTrace(ThrowableProxy throwableProxy) {
if (throwableProxy == null) {
return null;
}
if (this.stackTracePrinter != null) {
Throwable throwable = throwableProxy.getThrowable();
Assert.state(throwable != null, "Proxy must return Throwable in order to print exception");
return this.stackTracePrinter.printStackTraceToString(throwable);
}
return throwableProxy.getExtendedStackTraceAsString();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -34,6 +34,7 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.WritableJson;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
@ -52,6 +53,7 @@ import org.springframework.util.StringUtils;
*
* @author Samuel Lissner
* @author Moritz Halbritter
* @author Phillip Webb
*/
class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
@ -69,12 +71,14 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
*/
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id");
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment,
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, members), customizer);
super((members) -> jsonMembers(environment, stackTracePrinter, members), customizer);
}
private static void jsonMembers(Environment environment, JsonWriter.Members<LogEvent> members) {
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
JsonWriter.Members<LogEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter);
members.add("version", "1.1");
members.add("short_message", LogEvent::getMessage)
.as(GraylogExtendedLogFormatStructuredLogFormatter::getMessageText);
@ -92,7 +96,7 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
.usingPairs(GraylogExtendedLogFormatStructuredLogFormatter::createAdditionalFields);
members.add()
.whenNotNull(LogEvent::getThrownProxy)
.usingMembers(GraylogExtendedLogFormatStructuredLogFormatter::throwableMembers);
.usingMembers((thrownProxyMembers) -> throwableMembers(thrownProxyMembers, extractor));
}
private static String getMessageText(Message message) {
@ -122,20 +126,15 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
return Severity.getSeverity(event.getLevel()).getCode();
}
private static void throwableMembers(Members<LogEvent> members) {
members.add("full_message", GraylogExtendedLogFormatStructuredLogFormatter::formatFullMessageWithThrowable);
private static void throwableMembers(Members<LogEvent> members, Extractor extractor) {
members.add("full_message", extractor::messageAndStackTrace);
members.add("_error_type", (event) -> event.getThrownProxy().getThrowable())
.whenNotNull()
.as(ObjectUtils::nullSafeClassName);
members.add("_error_stack_trace", (event) -> event.getThrownProxy().getExtendedStackTraceAsString());
members.add("_error_stack_trace", extractor::stackTrace);
members.add("_error_message", (event) -> event.getThrownProxy().getMessage());
}
private static String formatFullMessageWithThrowable(LogEvent event) {
return event.getMessage().getFormattedMessage() + "\n\n"
+ event.getThrownProxy().getExtendedStackTraceAsString();
}
private static void createAdditionalFields(ReadOnlyStringMap contextData, BiConsumer<Object, Object> pairs) {
contextData.forEach((name, value) -> createAdditionalField(name, value, pairs));
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -25,11 +25,11 @@ import java.util.TreeSet;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
@ -44,11 +44,13 @@ import org.springframework.util.CollectionUtils;
*/
class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
LogstashStructuredLogFormatter(StructuredLoggingJsonMembersCustomizer<?> customizer) {
super(LogstashStructuredLogFormatter::jsonMembers, customizer);
LogstashStructuredLogFormatter(StackTracePrinter stackTracePrinter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(stackTracePrinter, members), customizer);
}
private static void jsonMembers(JsonWriter.Members<LogEvent> members) {
private static void jsonMembers(StackTracePrinter stackTracePrinter, JsonWriter.Members<LogEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter);
members.add("@timestamp", LogEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
members.add("@version", "1");
members.add("message", LogEvent::getMessage).as(StructuredMessage::get);
@ -63,9 +65,7 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<Lo
.whenNotNull()
.as(LogstashStructuredLogFormatter::getMarkers)
.whenNot(CollectionUtils::isEmpty);
members.add("stack_trace", LogEvent::getThrownProxy)
.whenNotNull()
.as(ThrowableProxy::getExtendedStackTraceAsString);
members.add("stack_trace", LogEvent::getThrownProxy).whenNotNull().as(extractor::stackTrace);
}
private static String asTimestamp(Instant instant) {

View File

@ -29,6 +29,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.config.plugins.PluginLoggerContext;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
@ -111,22 +112,26 @@ final class StructuredLogLayout extends AbstractStringLayout {
private ElasticCommonSchemaStructuredLogFormatter createEcsFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new ElasticCommonSchemaStructuredLogFormatter(environment, jsonMembersCustomizer);
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, jsonMembersCustomizer);
}
private GraylogExtendedLogFormatStructuredLogFormatter createGraylogFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, jsonMembersCustomizer);
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, stackTracePrinter,
jsonMembersCustomizer);
}
private LogstashStructuredLogFormatter createLogstashFormatter(Instantiator<?> instantiator) {
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new LogstashStructuredLogFormatter(jsonMembersCustomizer);
return new LogstashStructuredLogFormatter(stackTracePrinter, jsonMembersCustomizer);
}
}

View File

@ -30,6 +30,7 @@ import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
@ -49,13 +50,14 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
ElasticCommonSchemaStructuredLogFormatter(Environment environment, ThrowableProxyConverter throwableProxyConverter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, throwableProxyConverter, members), customizer);
ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, throwableProxyConverter, members), customizer);
}
private static void jsonMembers(Environment environment, ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter);
members.add("@timestamp", ILoggingEvent::getInstant);
members.add("log.level", ILoggingEvent::getLevel);
members.add("process.pid", environment.getProperty("spring.application.pid", Long.class))
@ -71,7 +73,7 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
members.add().whenNotNull(ILoggingEvent::getThrowableProxy).usingMembers((throwableMembers) -> {
throwableMembers.add("error.type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
throwableMembers.add("error.message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
throwableMembers.add("error.stack_trace", throwableProxyConverter::convert);
throwableMembers.add("error.stack_trace", extractor::stackTrace);
});
members.add("ecs.version", "8.11");
members.add("tags", ILoggingEvent::getMarkerList)

View File

@ -0,0 +1,58 @@
/*
* 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.logback;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.util.Assert;
/**
* Functions to extract items from {@link ILoggingEvent}.
*
* @author Phillip Webb
*/
class Extractor {
private final StackTracePrinter stackTracePrinter;
private final ThrowableProxyConverter throwableProxyConverter;
Extractor(StackTracePrinter stackTracePrinter, ThrowableProxyConverter throwableProxyConverter) {
this.stackTracePrinter = stackTracePrinter;
this.throwableProxyConverter = throwableProxyConverter;
}
String messageAndStackTrace(ILoggingEvent event) {
return event.getFormattedMessage() + "\n\n" + stackTrace(event);
}
String stackTrace(ILoggingEvent event) {
if (this.stackTracePrinter != null) {
IThrowableProxy throwableProxy = event.getThrowableProxy();
Assert.state(throwableProxy instanceof ThrowableProxy,
"Instance must be a ThrowableProxy in order to print exception");
Throwable throwable = ((ThrowableProxy) throwableProxy).getThrowable();
return this.stackTracePrinter.printStackTraceToString(throwable);
}
return this.throwableProxyConverter.convert(event);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -34,6 +34,7 @@ import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.WritableJson;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
@ -52,6 +53,7 @@ import org.springframework.util.StringUtils;
*
* @author Samuel Lissner
* @author Moritz Halbritter
* @author Phillip Webb
*/
class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {
@ -69,13 +71,14 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
*/
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id");
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment,
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, throwableProxyConverter, members), customizer);
super((members) -> jsonMembers(environment, stackTracePrinter, throwableProxyConverter, members), customizer);
}
private static void jsonMembers(Environment environment, ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter);
members.add("version", "1.1");
members.add("short_message", ILoggingEvent::getFormattedMessage)
.as(GraylogExtendedLogFormatStructuredLogFormatter::getMessageText);
@ -96,7 +99,7 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
.usingPairs(GraylogExtendedLogFormatStructuredLogFormatter::createAdditionalField);
members.add()
.whenNotNull(ILoggingEvent::getThrowableProxy)
.usingMembers((throwableMembers) -> throwableMembers(throwableMembers, throwableProxyConverter));
.usingMembers((throwableMembers) -> throwableMembers(throwableMembers, extractor));
}
private static String getMessageText(String formattedMessage) {
@ -115,19 +118,13 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
return (out) -> out.append(new BigDecimal(timeStamp).movePointLeft(3).toPlainString());
}
private static void throwableMembers(Members<ILoggingEvent> members,
ThrowableProxyConverter throwableProxyConverter) {
members.add("full_message", (event) -> formatFullMessageWithThrowable(throwableProxyConverter, event));
private static void throwableMembers(Members<ILoggingEvent> members, Extractor extractor) {
members.add("full_message", extractor::messageAndStackTrace);
members.add("_error_type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
members.add("_error_stack_trace", throwableProxyConverter::convert);
members.add("_error_stack_trace", extractor::stackTrace);
members.add("_error_message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
}
private static String formatFullMessageWithThrowable(ThrowableProxyConverter throwableProxyConverter,
ILoggingEvent event) {
return event.getFormattedMessage() + "\n\n" + throwableProxyConverter.convert(event);
}
private static void createAdditionalField(List<KeyValuePair> keyValuePairs, BiConsumer<Object, Object> pairs) {
keyValuePairs.forEach((keyValuePair) -> createAdditionalField(keyValuePair.key, keyValuePair.value, pairs));
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -33,6 +33,7 @@ import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
@ -49,13 +50,14 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<IL
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
LogstashStructuredLogFormatter(ThrowableProxyConverter throwableProxyConverter,
LogstashStructuredLogFormatter(StackTracePrinter stackTracePrinter, ThrowableProxyConverter throwableProxyConverter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(throwableProxyConverter, members), customizer);
super((members) -> jsonMembers(stackTracePrinter, throwableProxyConverter, members), customizer);
}
private static void jsonMembers(ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
private static void jsonMembers(StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter);
members.add("@timestamp", ILoggingEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
members.add("@version", "1");
members.add("message", ILoggingEvent::getFormattedMessage);
@ -73,7 +75,7 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<IL
.whenNotEmpty();
members.add("stack_trace", (event) -> event)
.whenNotNull(ILoggingEvent::getThrowableProxy)
.as(throwableProxyConverter::convert);
.as(extractor::stackTrace);
}
private static String asTimestamp(Instant instant) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -24,6 +24,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.encoder.EncoderBase;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
@ -88,27 +89,30 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
private StructuredLogFormatter<ILoggingEvent> createEcsFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new ElasticCommonSchemaStructuredLogFormatter(environment, throwableProxyConverter,
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, throwableProxyConverter,
jsonMembersCustomizer);
}
private StructuredLogFormatter<ILoggingEvent> createGraylogFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, throwableProxyConverter,
jsonMembersCustomizer);
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, stackTracePrinter,
throwableProxyConverter, jsonMembersCustomizer);
}
private StructuredLogFormatter<ILoggingEvent> createLogstashFormatter(Instantiator<?> instantiator) {
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new LogstashStructuredLogFormatter(throwableProxyConverter, jsonMembersCustomizer);
return new LogstashStructuredLogFormatter(stackTracePrinter, throwableProxyConverter, jsonMembersCustomizer);
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -20,6 +20,7 @@ import java.nio.charset.Charset;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.core.env.Environment;
/**
@ -29,6 +30,7 @@ import org.springframework.core.env.Environment;
* <ul>
* <li>{@link Environment}</li>
* <li>{@link StructuredLoggingJsonMembersCustomizer}</li>
* <li>{@link StackTracePrinter} (may be {@code null})</li>
* </ul>
* When using Logback, implementing classes can also use the following parameter types in
* the constructor:

View File

@ -24,6 +24,7 @@ import java.util.TreeMap;
import java.util.function.Consumer;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.util.Instantiator;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.boot.util.Instantiator.FailureHandler;
@ -77,12 +78,14 @@ public class StructuredLogFormatterFactory<E> {
StructuredLogFormatterFactory(SpringFactoriesLoader factoriesLoader, Class<E> logEventType, Environment environment,
Consumer<AvailableParameters> availableParameters, Consumer<CommonFormatters<E>> commonFormatters) {
StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment);
this.factoriesLoader = factoriesLoader;
this.logEventType = logEventType;
this.instantiator = new Instantiator<>(Object.class, (allAvailableParameters) -> {
allAvailableParameters.add(Environment.class, environment);
allAvailableParameters.add(StructuredLoggingJsonMembersCustomizer.class,
(type) -> getStructuredLoggingJsonMembersCustomizer(environment));
(type) -> getStructuredLoggingJsonMembersCustomizer(properties));
allAvailableParameters.add(StackTracePrinter.class, (type) -> getStackTracePrinter(properties));
if (availableParameters != null) {
availableParameters.accept(allAvailableParameters);
}
@ -91,9 +94,9 @@ public class StructuredLogFormatterFactory<E> {
commonFormatters.accept(this.commonFormatters);
}
StructuredLoggingJsonMembersCustomizer<?> getStructuredLoggingJsonMembersCustomizer(Environment environment) {
StructuredLoggingJsonMembersCustomizer<?> getStructuredLoggingJsonMembersCustomizer(
StructuredLoggingJsonProperties properties) {
List<StructuredLoggingJsonMembersCustomizer<?>> customizers = new ArrayList<>();
StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment);
if (properties != null) {
customizers.add(new StructuredLoggingJsonPropertiesJsonMembersCustomizer(this.instantiator, properties));
}
@ -115,6 +118,10 @@ public class StructuredLogFormatterFactory<E> {
}
}
private StackTracePrinter getStackTracePrinter(StructuredLoggingJsonProperties properties) {
return (properties != null && properties.stackTrace() != null) ? properties.stackTrace().createPrinter() : null;
}
/**
* Get a new {@link StructuredLogFormatter} instance for the specified format.
* @param format the format requested (either a {@link CommonStructuredLogFormat} ID

View File

@ -19,11 +19,19 @@ package org.springframework.boot.logging.structured;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.StandardStackTracePrinter;
import org.springframework.boot.util.Instantiator;
import org.springframework.core.env.Environment;
@ -34,13 +42,15 @@ import org.springframework.core.env.Environment;
* @param exclude the paths that should be excluded. An empty set excludes nothing
* @param rename a map of path to replacement names
* @param add a map of additional elements {@link StructuredLoggingJsonMembersCustomizer}
* @param stackTrace stack trace properties
* @param customizer the fully qualified names of
* {@link StructuredLoggingJsonMembersCustomizer} implementations
* @author Phillip Webb
* @author Yanming Zhou
*/
record StructuredLoggingJsonProperties(Set<String> include, Set<String> exclude, Map<String, String> rename,
Map<String, String> add, Set<Class<? extends StructuredLoggingJsonMembersCustomizer<?>>> customizer) {
Map<String, String> add, StackTrace stackTrace,
Set<Class<? extends StructuredLoggingJsonMembersCustomizer<?>>> customizer) {
StructuredLoggingJsonProperties {
customizer = (customizer != null) ? customizer : Collections.emptySet();
@ -57,6 +67,74 @@ record StructuredLoggingJsonProperties(Set<String> include, Set<String> exclude,
.orElse(null);
}
/**
* Properties to influence stack trace printing.
*
* @param printer the name of the printer to use. Can be {@code null},
* {@code "standard"}, {@code "logging-system"}, or the fully-qualified class name of
* a {@link StackTracePrinter} implementation. A {@code null} value will be treated as
* {@code "standard"} when any other property is set, otherwise it will be treated as
* {@code "logging-system"}. {@link StackTracePrinter} implementations may optionally
* inject a {@link StandardStackTracePrinter} instance into their constructor which
* will be configured from the properties.
* @param root the root ordering (root first or root last)
* @param maxLength the maximum length to print
* @param maxThrowableDepth the maximum throwable depth to print
* @param includeCommonFrames whether common frames should be included
* @param includeHashes whether stack trace hashes should be included
*/
record StackTrace(String printer, Root root, Integer maxLength, Integer maxThrowableDepth,
Boolean includeCommonFrames, Boolean includeHashes) {
StackTracePrinter createPrinter() {
String name = (printer() != null) ? printer() : "";
name = name.toLowerCase(Locale.getDefault()).replace("-", "");
if ("loggingsystem".equals(name) || (name.isEmpty() && !hasAnyOtherProperty())) {
return null;
}
StandardStackTracePrinter standardPrinter = createStandardPrinter();
if ("standard".equals(name) || name.isEmpty()) {
return standardPrinter;
}
return (StackTracePrinter) new Instantiator<>(StackTracePrinter.class,
(parameters) -> parameters.add(StandardStackTracePrinter.class, standardPrinter))
.instantiate(printer());
}
private boolean hasAnyOtherProperty() {
return Stream.of(root(), maxLength(), maxThrowableDepth(), includeCommonFrames(), includeHashes())
.anyMatch(Objects::nonNull);
}
private StandardStackTracePrinter createStandardPrinter() {
StandardStackTracePrinter printer = (root() != Root.FIRST) ? StandardStackTracePrinter.rootFirst()
: StandardStackTracePrinter.rootLast();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
printer = map.from(this::maxLength).to(printer, StandardStackTracePrinter::withMaximumLength);
printer = map.from(this::maxThrowableDepth)
.to(printer, StandardStackTracePrinter::withMaximumThrowableDepth);
printer = map.from(this::includeCommonFrames)
.to(printer, apply(StandardStackTracePrinter::withCommonFrames));
printer = map.from(this::includeHashes).to(printer, apply(StandardStackTracePrinter::withHashes));
return printer;
}
private BiFunction<StandardStackTracePrinter, Boolean, StandardStackTracePrinter> apply(
UnaryOperator<StandardStackTracePrinter> action) {
return (printer, value) -> (!value) ? printer : action.apply(printer);
}
/**
* Root ordering.
*/
enum Root {
LAST, FIRST
}
}
static class StructuredLoggingJsonPropertiesRuntimeHints extends BindableRuntimeHintsRegistrar {
StructuredLoggingJsonPropertiesRuntimeHints() {

View File

@ -286,6 +286,36 @@
"type": "java.util.Map<java.lang.String,java.lang.String>",
"description": "Mapping between member paths and an alternative name that should be used in structured logging JSON"
},
{
"name": "logging.structured.json.stacktrace.include-common-frames",
"type": "java.lang.Boolean",
"description": "Whether common frames should be included."
},
{
"name": "logging.structured.json.stacktrace.include-hashes",
"type": "java.lang.Boolean",
"description": "Whether stack trace hashes should be included."
},
{
"name": "logging.structured.json.stacktrace.max-length",
"type": "java.lang.Integer",
"description": "Maximum length to print."
},
{
"name": "logging.structured.json.stacktrace.max-throwable-depth",
"type": "java.lang.Integer",
"description": "Maximum throwable depth to print."
},
{
"name": "logging.structured.json.stacktrace.printer",
"type": "java.lang.String",
"description": "Name of the printer to use. Can be 'standard', 'logging-system', or the fully-qualified class name of a StackTracePrinter. When not specified 'logging-system' or 'standard' will be used depening if other properties are set."
},
{
"name": "logging.structured.json.stacktrace.root",
"type": "org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StackTrace.Root",
"description": "Root ordering (root first or root last)."
},
{
"name": "logging.threshold.console",
"type": "java.lang.String",
@ -697,6 +727,25 @@
}
}
]
},
{
"name": "logging.structured.json.stacktrace.printer",
"values": [
{
"value": "standard"
},
{
"value": "logging-system"
}
],
"providers": [
{
"name": "handle-as",
"parameters": {
"target": "java.lang.Class<org.springframework.boot.logging.StackTracePrinter>"
}
}
]
},
{
"name": "spring.config.import",

View File

@ -0,0 +1,390 @@
/*
* 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.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Objects;
import org.junit.jupiter.api.Test;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link StandardStackTracePrinter}.
*
* @author Phillip Webb
*/
class StandardStackTracePrinterTests {
@Test
void rootLastPrintsStackTrace() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast();
assertThatCleanedStackTraceMatches(printer, exception, standardStackTrace());
}
@Test
void rootLastPrintsStackTraceThatMatchesJvm() {
Throwable exception = TestException.create();
Writer printedJvmStackTrace = new StringWriter();
exception.printStackTrace(new PrintWriter(printedJvmStackTrace));
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast();
assertThatCleanedStackTraceMatches(printer, exception,
TestException.withoutLineNumbers(printedJvmStackTrace.toString()));
}
@Test
void rootFirstPrintsStackTrace() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst();
assertThatCleanedStackTraceMatches(printer, exception, """
java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Wrapped by: java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Wrapped by: java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Suppressed: java.lang.RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
""");
}
@Test
void withCommonFramesWhenRootLastPrintsAllFrames() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast().withCommonFrames();
assertThatCleanedStackTraceMatches(printer, exception, """
java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Suppressed: java.lang.RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Caused by: java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Caused by: java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
""");
}
@Test
void withCommonFramesWhenRootFirstPrintsAllFrames() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst().withCommonFrames();
assertThatCleanedStackTraceMatches(printer, exception, """
java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Wrapped by: java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Wrapped by: java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Suppressed: java.lang.RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
""");
}
@Test
void withoutSuppressedHidesSuppressed() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast().withoutSuppressed();
assertThatCleanedStackTraceMatches(printer, exception, """
java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Caused by: java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Caused by: java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
""");
}
@Test
void withMaximumLengthWhenNegativeThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StandardStackTracePrinter.rootFirst().withMaximumLength(0))
.withMessage("'maximumLength' must be positive");
}
@Test
void withMaximumLengthTruncatesOutput() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst().withMaximumLength(14);
assertThat(printer.printStackTraceToString(exception)).isEqualTo("java.lang.R...");
}
@Test
void withMaximumThrowableDepthWhenNegativeThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StandardStackTracePrinter.rootFirst().withMaximumThrowableDepth(0))
.withMessage("'maximumThrowableDepth' must be positive");
}
@Test
void withMaximumThrowableDepthFiltersElements() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst().withMaximumThrowableDepth(1);
assertThatCleanedStackTraceMatches(printer, exception, """
java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Wrapped by: java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
... 1 filtered
... 1 more
Wrapped by: java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
... 3 filtered
Suppressed: java.lang.RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
""");
}
@Test
void withMaximumThrowableDepthAndCommonFramesFiltersElements() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst()
.withCommonFrames()
.withMaximumThrowableDepth(2);
assertThatCleanedStackTraceMatches(printer, exception, """
java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Wrapped by: java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 filtered
Wrapped by: java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
... 2 filtered
Suppressed: java.lang.RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
""");
}
@Test
void withFilterWhenPredicateIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> StandardStackTracePrinter.rootFirst().withFilter(null))
.withMessage("'predicate' must not be null");
}
@Test
void withFilterWhenFilterMatches() {
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst()
.withFilter(IllegalStateException.class::isInstance);
assertThat(printer.printStackTraceToString(new IllegalStateException("test"))).isNotEmpty();
}
@Test
void withFilterWhenFilterDoesNotMatch() {
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst()
.withFilter(IllegalStateException.class::isInstance);
assertThat(printer.printStackTraceToString(new RuntimeException("test"))).isEmpty();
}
@Test
void withMultipleFiltersMustAllMatch() {
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst()
.withFilter(IllegalStateException.class::isInstance)
.withFilter((ex) -> "test".equals(ex.getMessage()));
assertThat(printer.printStackTraceToString(new IllegalStateException("test"))).isNotEmpty();
assertThat(printer.printStackTraceToString(new IllegalStateException("nope"))).isEmpty();
assertThat(printer.printStackTraceToString(new RuntimeException("test"))).isEmpty();
}
@Test
void withFrameFilter() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootFirst()
.withCommonFrames()
.withFrameFilter((index, element) -> element.getMethodName().startsWith("run"));
assertThatCleanedStackTraceMatches(printer, exception, """
java.lang.RuntimeException: root
... 1 filtered
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Wrapped by: java.lang.RuntimeException: cause
... 2 filtered
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Wrapped by: java.lang.RuntimeException: exception
... 3 filtered
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Suppressed: java.lang.RuntimeException: supressed
... 1 filtered
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
""");
}
@Test
void withLineSeparatorUsesLineSeparator() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast().withLineSeparator("!");
assertThatCleanedStackTraceMatches(printer, exception,
standardStackTrace().replace(System.lineSeparator(), "!"));
}
@Test
void withFormatterWhenFormatterIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> StandardStackTracePrinter.rootLast().withFormatter(null))
.withMessage("'formatter' must not be null");
}
@Test
void withFormatterFormatsThrowable() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast()
.withFormatter((throwable) -> ClassUtils.getShortName(throwable.getClass()) + ": "
+ throwable.getLocalizedMessage());
assertThatCleanedStackTraceMatches(printer, exception, """
RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Suppressed: RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Caused by: RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Caused by: RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
""");
}
@Test
void withFrameFormatterWhenFormatterIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StandardStackTracePrinter.rootLast().withFrameFormatter(null))
.withMessage("'frameFormatter' must not be null");
}
@Test
void withFrameFormatterFormatsFrame() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast()
.withFrameFormatter(
(element) -> ClassUtils.getShortName(element.getClassName()) + "." + element.getMethodName());
assertThat(printer.printStackTraceToString(exception)).isEqualTo("""
java.lang.RuntimeException: exception
at TestException.actualCreateException
at TestException.createException
at TestException.createTestException
at TestException.CreatorThread.run
Suppressed: java.lang.RuntimeException: supressed
at TestException.createTestException
... 1 more
Caused by: java.lang.RuntimeException: cause
at TestException.createCause
at TestException.createTestException
... 1 more
Caused by: java.lang.RuntimeException: root
at TestException.createTestException
... 1 more
""");
}
@Test
void withHashesFunctionPrintsStackTraceWithHashes() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast()
.withHashes((frame) -> Objects.hash(frame.getClassName(), frame.getMethodName()));
assertThat(printer.printStackTraceToString(exception)).isEqualTo("""
<#cc3eebec> java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:63)
at org.springframework.boot.logging.TestException.createException(TestException.java:59)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:49)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:77)
Suppressed: <#834defc3> java.lang.RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:50)
... 1 more
Caused by: <#611639c5> java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:55)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:48)
... 1 more
Caused by: <#834defc3> java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:47)
... 1 more
""");
}
@Test
void withHashesPrintsStackTraceWithHashes() {
Throwable exception = TestException.create();
StandardStackTracePrinter printer = StandardStackTracePrinter.rootLast().withHashes();
assertThat(printer.printStackTraceToString(exception)).containsPattern("<#[0-9a-f]{8}>");
}
private String standardStackTrace() {
return """
java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
at org.springframework.boot.logging.TestException$CreatorThread.run(TestException.java:NN)
Suppressed: java.lang.RuntimeException: supressed
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Caused by: java.lang.RuntimeException: cause
at org.springframework.boot.logging.TestException.createCause(TestException.java:NN)
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
Caused by: java.lang.RuntimeException: root
at org.springframework.boot.logging.TestException.createTestException(TestException.java:NN)
... 1 more
""";
}
private void assertThatCleanedStackTraceMatches(StandardStackTracePrinter printer, Throwable throwable,
String expected) {
String actual = printer.printStackTraceToString(throwable);
assertThat(TestException.withoutLineNumbers(actual)).isEqualTo(expected);
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Creates a test exception without too much stacktrace.
*
* @author Phillip Webb
*/
public final class TestException {
private static final Pattern LINE_NUMBER_PATTERN = Pattern.compile("\\.java\\:\\d+\\)");
private TestException() {
}
public static Exception create() {
CreatorThread creatorThread = new CreatorThread();
creatorThread.start();
try {
creatorThread.join();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return creatorThread.exception;
}
private static Exception createTestException() {
Throwable root = new RuntimeException("root");
Throwable cause = createCause(root);
Exception exception = createException(cause);
exception.addSuppressed(new RuntimeException("supressed"));
return exception;
}
private static Throwable createCause(Throwable root) {
return new RuntimeException("cause", root);
}
private static Exception createException(Throwable cause) {
return actualCreateException(cause);
}
private static Exception actualCreateException(Throwable cause) {
return new RuntimeException("exception", cause);
}
public static String withoutLineNumbers(String stackTrace) {
Matcher matcher = LINE_NUMBER_PATTERN.matcher(stackTrace);
return matcher.replaceAll(".java:NN)");
}
private static final class CreatorThread extends Thread {
Exception exception;
@Override
public void run() {
this.exception = createTestException();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -60,12 +60,17 @@ abstract class AbstractStructuredLoggingTests {
}
protected static MutableLogEvent createEvent() {
return createEvent(null);
}
protected static MutableLogEvent createEvent(Throwable thrown) {
MutableLogEvent event = new MutableLogEvent();
event.setTimeMillis(EVENT_TIME.toEpochMilli());
event.setLevel(Level.INFO);
event.setThreadName("main");
event.setLoggerName("org.example.Test");
event.setMessage(new SimpleMessage("message"));
event.setThrown(thrown);
return event;
}

View File

@ -37,20 +37,23 @@ import static org.mockito.BDDMockito.then;
* Tests for {@link ElasticCommonSchemaStructuredLogFormatter}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private MockEnvironment environment;
private ElasticCommonSchemaStructuredLogFormatter formatter;
@BeforeEach
void setUp() {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("logging.structured.ecs.service.name", "name");
environment.setProperty("logging.structured.ecs.service.version", "1.0.0");
environment.setProperty("logging.structured.ecs.service.environment", "test");
environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
environment.setProperty("spring.application.pid", "1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(environment, this.customizer);
this.environment = new MockEnvironment();
this.environment.setProperty("logging.structured.ecs.service.name", "name");
this.environment.setProperty("logging.structured.ecs.service.version", "1.0.0");
this.environment.setProperty("logging.structured.ecs.service.environment", "test");
this.environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, null, this.customizer);
}
@Test
@ -89,6 +92,17 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException""");
}
@Test
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(),
this.customizer);
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
Map<String, Object> deserialized = deserialize(this.formatter.format(event));
String stackTrace = (String) deserialized.get("error.stack_trace");
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
}
@Test
void shouldFormatStructuredMessage() {
MutableLogEvent event = createEvent();

View File

@ -0,0 +1,66 @@
/*
* 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.log4j2;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.apache.logging.log4j.message.SimpleMessage;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Extractor}.
*
* @author Phillip Webb
*/
class ExtractorTests {
@Test
void messageAndStackTraceWhenNoPrinterPrintsUsingLoggingSystem() {
Extractor extractor = new Extractor(null);
assertThat(extractor.messageAndStackTrace(createEvent())).startsWith("TestMessage\n\n")
.contains("java.lang.RuntimeException: Boom!");
}
@Test
void messageAndStackTraceWhenPrinterPrintsUsingPrinter() {
Extractor extractor = new Extractor(new SimpleStackTracePrinter());
assertThat(extractor.messageAndStackTrace(createEvent()))
.isEqualTo("TestMessage\n\nstacktrace:RuntimeException");
}
@Test
void stackTraceWhenNoPrinterPrintsUsingLoggingSystem() {
Extractor extractor = new Extractor(null);
assertThat(extractor.stackTrace(createEvent())).contains("java.lang.RuntimeException: Boom!");
}
@Test
void stackTraceWhenPrinterPrintsUsingPrinter() {
Extractor extractor = new Extractor(new SimpleStackTracePrinter());
assertThat(extractor.stackTrace(createEvent())).isEqualTo("stacktrace:RuntimeException");
}
private LogEvent createEvent() {
MutableLogEvent event = new MutableLogEvent();
event.setMessage(new SimpleMessage("TestMessage"));
event.setThrown(new RuntimeException("Boom!"));
return event;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -37,19 +37,22 @@ import static org.mockito.BDDMockito.then;
*
* @author Samuel Lissner
* @author Moritz Halbritter
* @author Phillip Webb
*/
@ExtendWith(OutputCaptureExtension.class)
class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private GraylogExtendedLogFormatStructuredLogFormatter formatter;
private MockEnvironment environment;
@BeforeEach
void setUp() {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("logging.structured.gelf.host", "name");
environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
environment.setProperty("spring.application.pid", "1");
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment, this.customizer);
this.environment = new MockEnvironment();
this.environment.setProperty("logging.structured.gelf.host", "name");
this.environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment, null, this.customizer);
}
@Test
@ -148,4 +151,20 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
message\\n\\njava.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.GraylogExtendedLogFormatStructuredLogFormatterTests.shouldFormatException""");
}
@Test
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment,
new SimpleStackTracePrinter(), this.customizer);
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
String fullMessage = (String) deserialized.get("full_message");
String stackTrace = (String) deserialized.get("_error_stack_trace");
assertThat(fullMessage).isEqualTo("message\n\nstacktrace:RuntimeException");
assertThat(deserialized)
.containsAllEntriesOf(map("_error_type", "java.lang.RuntimeException", "_error_message", "Boom"));
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -37,6 +37,7 @@ import static org.mockito.BDDMockito.then;
* Tests for {@link LogstashStructuredLogFormatter}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
@ -44,7 +45,7 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
@BeforeEach
void setUp() {
this.formatter = new LogstashStructuredLogFormatter(this.customizer);
this.formatter = new LogstashStructuredLogFormatter(null, this.customizer);
}
@Test
@ -85,6 +86,17 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.LogstashStructuredLogFormatterTests.shouldFormatException""");
}
@Test
void shouldFormatExceptionWithStackTracePrinter() {
this.formatter = new LogstashStructuredLogFormatter(new SimpleStackTracePrinter(), this.customizer);
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
String stackTrace = (String) deserialized.get("stack_trace");
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
}
@Test
void shouldFormatStructuredMessage() {
MutableLogEvent event = createEvent();

View File

@ -0,0 +1,37 @@
/*
* 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.log4j2;
import java.io.IOException;
import com.mchange.v1.lang.ClassUtils;
import org.springframework.boot.logging.StackTracePrinter;
/**
* Simple {@link StackTracePrinter} used for testing.
*
* @author Phillip Webb
*/
class SimpleStackTracePrinter implements StackTracePrinter {
@Override
public void printStackTrace(Throwable throwable, Appendable out) throws IOException {
out.append("stacktrace:" + ClassUtils.simpleClassName(throwable.getClass()));
}
}

View File

@ -39,8 +39,9 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* Tests for {@link StructuredLogLayout}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
class StructuredLogLayoutTests extends AbstractStructuredLoggingTests {
private MockEnvironment environment;
@ -49,6 +50,8 @@ class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
@BeforeEach
void setup() {
this.environment = new MockEnvironment();
this.environment.setProperty("logging.structured.json.stacktrace.printer",
SimpleStackTracePrinter.class.getName());
this.loggerContext = (LoggerContext) LogManager.getContext(false);
this.loggerContext.putObject(Log4J2LoggingSystem.ENVIRONMENT_KEY, this.environment);
}
@ -61,17 +64,28 @@ class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
@Test
void shouldSupportEcsCommonFormat() {
StructuredLogLayout layout = newBuilder().setFormat("ecs").build();
String json = layout.toSerializable(createEvent());
String json = layout.toSerializable(createEvent(new RuntimeException("Boom!")));
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("ecs.version");
assertThat(deserialized.get("error.stack_trace")).isEqualTo("stacktrace:RuntimeException");
}
@Test
void shouldSupportLogstashCommonFormat() {
StructuredLogLayout layout = newBuilder().setFormat("logstash").build();
String json = layout.toSerializable(createEvent());
String json = layout.toSerializable(createEvent(new RuntimeException("Boom!")));
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("@version");
assertThat(deserialized.get("stack_trace")).isEqualTo("stacktrace:RuntimeException");
}
@Test
void shouldSupportGelfCommonFormat() {
StructuredLogLayout layout = newBuilder().setFormat("gelf").build();
String json = layout.toSerializable(createEvent(new RuntimeException("Boom!")));
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("version");
assertThat(deserialized.get("_error_stack_trace")).isEqualTo("stacktrace:RuntimeException");
}
@Test

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -25,6 +25,7 @@ import java.util.Map;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -100,12 +101,19 @@ abstract class AbstractStructuredLoggingTests {
}
protected static LoggingEvent createEvent() {
return createEvent(null);
}
protected static LoggingEvent createEvent(Throwable thrown) {
LoggingEvent event = new LoggingEvent();
event.setInstant(EVENT_TIME);
event.setLevel(Level.INFO);
event.setThreadName("main");
event.setLoggerName("org.example.Test");
event.setMessage("message");
if (thrown != null) {
event.setThrowableProxy(new ThrowableProxy(thrown));
}
return event;
}

View File

@ -37,23 +37,26 @@ import static org.mockito.BDDMockito.then;
* Tests for {@link ElasticCommonSchemaStructuredLogFormatter}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private MockEnvironment environment;
private ElasticCommonSchemaStructuredLogFormatter formatter;
@Override
@BeforeEach
void setUp() {
super.setUp();
MockEnvironment environment = new MockEnvironment();
environment.setProperty("logging.structured.ecs.service.name", "name");
environment.setProperty("logging.structured.ecs.service.version", "1.0.0");
environment.setProperty("logging.structured.ecs.service.environment", "test");
environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
environment.setProperty("spring.application.pid", "1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(environment, getThrowableProxyConverter(),
this.customizer);
this.environment = new MockEnvironment();
this.environment.setProperty("logging.structured.ecs.service.name", "name");
this.environment.setProperty("logging.structured.ecs.service.version", "1.0.0");
this.environment.setProperty("logging.structured.ecs.service.environment", "test");
this.environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, null,
getThrowableProxyConverter(), this.customizer);
}
@Test
@ -95,6 +98,18 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
.replace("\r", "\\r"));
}
@Test
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(),
getThrowableProxyConverter(), this.customizer);
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
Map<String, Object> deserialized = deserialize(this.formatter.format(event));
String stackTrace = (String) deserialized.get("error.stack_trace");
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
}
@Test
void shouldFormatMarkersAsTags() {
LoggingEvent event = createEvent();

View File

@ -0,0 +1,73 @@
/*
* 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.logback;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Extractor}.
*
* @author Phillip Webb
*/
class ExtractorTests {
@Test
void messageAndStackTraceWhenNoPrinterPrintsUsingLoggingSystem() {
Extractor extractor = new Extractor(null, createConverter());
assertThat(extractor.messageAndStackTrace(createEvent())).startsWith("TestMessage\n\n")
.contains("java.lang.RuntimeException: Boom!");
}
@Test
void messageAndStackTraceWhenNoPrinterPrintsUsingPrinter() {
Extractor extractor = new Extractor(new SimpleStackTracePrinter(), createConverter());
assertThat(extractor.messageAndStackTrace(createEvent()))
.isEqualTo("TestMessage\n\nstacktrace:RuntimeException");
}
@Test
void stackTraceWhenNoPrinterPrintsUsingLoggingSystem() {
Extractor extractor = new Extractor(null, createConverter());
assertThat(extractor.stackTrace(createEvent())).contains("java.lang.RuntimeException: Boom!");
}
@Test
void stackTraceWhenNoPrinterPrintsUsingPrinter() {
Extractor extractor = new Extractor(new SimpleStackTracePrinter(), createConverter());
assertThat(extractor.stackTrace(createEvent())).isEqualTo("stacktrace:RuntimeException");
}
private ThrowableProxyConverter createConverter() {
ThrowableProxyConverter converter = new ThrowableProxyConverter();
converter.start();
return converter;
}
private ILoggingEvent createEvent() {
LoggingEvent event = new LoggingEvent();
event.setMessage("TestMessage");
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom!")));
return event;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -38,22 +38,25 @@ import static org.mockito.BDDMockito.then;
*
* @author Samuel Lissner
* @author Moritz Halbritter
* @author Phillip Webb
*/
@ExtendWith(OutputCaptureExtension.class)
class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private MockEnvironment environment;
private GraylogExtendedLogFormatStructuredLogFormatter formatter;
@Override
@BeforeEach
void setUp() {
super.setUp();
MockEnvironment environment = new MockEnvironment();
environment.setProperty("logging.structured.gelf.host", "name");
environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
environment.setProperty("spring.application.pid", "1");
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment, getThrowableProxyConverter(),
this.customizer);
this.environment = new MockEnvironment();
this.environment.setProperty("logging.structured.gelf.host", "name");
this.environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment, null,
getThrowableProxyConverter(), this.customizer);
}
@Test
@ -152,4 +155,21 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
.replace("\r", "\\r"));
}
@Test
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment,
new SimpleStackTracePrinter(), getThrowableProxyConverter(), this.customizer);
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
String fullMessage = (String) deserialized.get("full_message");
String stackTrace = (String) deserialized.get("_error_stack_trace");
assertThat(fullMessage).isEqualTo("message\n\nstacktrace:RuntimeException");
assertThat(deserialized)
.containsAllEntriesOf(map("_error_type", "java.lang.RuntimeException", "_error_message", "Boom"));
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -46,7 +46,7 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
@BeforeEach
void setUp() {
super.setUp();
this.formatter = new LogstashStructuredLogFormatter(getThrowableProxyConverter(), this.customizer);
this.formatter = new LogstashStructuredLogFormatter(null, getThrowableProxyConverter(), this.customizer);
}
@Test
@ -90,4 +90,17 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
.replace("\r", "\\r"));
}
@Test
void shouldFormatExceptionWithStackTracePrinter() {
this.formatter = new LogstashStructuredLogFormatter(new SimpleStackTracePrinter(), getThrowableProxyConverter(),
this.customizer);
LoggingEvent event = createEvent();
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
event.setMDCPropertyMap(Collections.emptyMap());
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
String stackTrace = (String) deserialized.get("stack_trace");
assertThat(stackTrace).isEqualTo("stacktrace:RuntimeException");
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.logback;
import java.io.IOException;
import com.mchange.v1.lang.ClassUtils;
import org.springframework.boot.logging.StackTracePrinter;
/**
* Simple {@link StackTracePrinter} used for testing.
*
* @author Phillip Webb
*/
class SimpleStackTracePrinter implements StackTracePrinter {
@Override
public void printStackTrace(Throwable throwable, Appendable out) throws IOException {
out.append("stacktrace:" + ClassUtils.simpleClassName(throwable.getClass()));
}
}

View File

@ -56,6 +56,8 @@ class StructuredLogEncoderTests extends AbstractStructuredLoggingTests {
void setUp() {
super.setUp();
this.environment = new MockEnvironment();
this.environment.setProperty("logging.structured.json.stacktrace.printer",
SimpleStackTracePrinter.class.getName());
this.loggerContext = new ContextBase();
this.loggerContext.putObject(Environment.class.getName(), this.environment);
this.encoder = new StructuredLogEncoder();
@ -73,22 +75,36 @@ class StructuredLogEncoderTests extends AbstractStructuredLoggingTests {
void shouldSupportEcsCommonFormat() {
this.encoder.setFormat("ecs");
this.encoder.start();
LoggingEvent event = createEvent();
LoggingEvent event = createEvent(new RuntimeException("Boom!"));
event.setMDCPropertyMap(Collections.emptyMap());
String json = encode(event);
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("ecs.version");
assertThat(deserialized.get("error.stack_trace")).isEqualTo("stacktrace:RuntimeException");
}
@Test
void shouldSupportLogstashCommonFormat() {
this.encoder.setFormat("logstash");
this.encoder.start();
LoggingEvent event = createEvent();
LoggingEvent event = createEvent(new RuntimeException("Boom!"));
event.setMDCPropertyMap(Collections.emptyMap());
String json = encode(event);
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("@version");
assertThat(deserialized.get("stack_trace")).isEqualTo("stacktrace:RuntimeException");
}
@Test
void shouldSupportGelfCommonFormat() {
this.encoder.setFormat("gelf");
this.encoder.start();
LoggingEvent event = createEvent(new RuntimeException("Boom!"));
event.setMDCPropertyMap(Collections.emptyMap());
String json = encode(event);
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("version");
assertThat(deserialized.get("_error_stack_trace")).isEqualTo("stacktrace:RuntimeException");
}
@Test

View File

@ -22,6 +22,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.JsonWriter.ValueProcessor;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.StandardStackTracePrinter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.core.env.Environment;
@ -60,7 +62,7 @@ class StructuredLogFormatterFactoryTests {
private void addCommonFormatters(CommonFormatters<LogEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new TestEcsFormatter(instantiator.getArg(Environment.class),
instantiator.getArg(StringBuilder.class)));
instantiator.getArg(StackTracePrinter.class), instantiator.getArg(StringBuilder.class)));
}
@Test
@ -94,11 +96,20 @@ class StructuredLogFormatterFactoryTests {
}
@Test
void getUsingClassNameInjectsApplicationMetadata() {
void getUsingClassNameInjectsEnvironment() {
TestEcsFormatter formatter = (TestEcsFormatter) this.factory.get(TestEcsFormatter.class.getName());
assertThat(formatter.getEnvironment()).isSameAs(this.environment);
}
@Test
void getUsingClassNameInjectsStackTracePrinter() {
this.environment.setProperty("logging.structured.json.stacktrace.printer", "standard");
StructuredLogFormatterFactory<LogEvent> factory = new StructuredLogFormatterFactory<>(LogEvent.class,
this.environment, this::addAvailableParameters, this::addCommonFormatters);
TestEcsFormatter formatter = (TestEcsFormatter) factory.get(TestEcsFormatter.class.getName());
assertThat(formatter.getStackTracePrinter()).isInstanceOf(StandardStackTracePrinter.class);
}
@Test
void getUsingClassNameInjectsCustomParameter() {
TestEcsFormatter formatter = (TestEcsFormatter) this.factory.get(TestEcsFormatter.class.getName());
@ -159,12 +170,15 @@ class StructuredLogFormatterFactoryTests {
static class TestEcsFormatter implements StructuredLogFormatter<LogEvent> {
private Environment environment;
private final Environment environment;
private StringBuilder custom;
private final StackTracePrinter stackTracePrinter;
TestEcsFormatter(Environment environment, StringBuilder custom) {
private final StringBuilder custom;
TestEcsFormatter(Environment environment, StackTracePrinter stackTracePrinter, StringBuilder custom) {
this.environment = environment;
this.stackTracePrinter = stackTracePrinter;
this.custom = custom;
}
@ -177,6 +191,10 @@ class StructuredLogFormatterFactoryTests {
return this.environment;
}
StackTracePrinter getStackTracePrinter() {
return this.stackTracePrinter;
}
StringBuilder getCustom() {
return this.custom;
}
@ -185,8 +203,8 @@ class StructuredLogFormatterFactoryTests {
static class ExtendedTestEcsFormatter extends TestEcsFormatter {
ExtendedTestEcsFormatter(Environment environment, StringBuilder custom) {
super(environment, custom);
ExtendedTestEcsFormatter(Environment environment, StackTracePrinter stackTracePrinter, StringBuilder custom) {
super(environment, stackTracePrinter, custom);
}
}

View File

@ -48,7 +48,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasExcludeFiltersMember() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null);
Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).doesNotContain("a").contains("b");
@ -57,7 +57,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasIncludeFiltersOtherMembers() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a"),
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null);
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("a")
@ -69,7 +69,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasIncludeAndExcludeFiltersMembers() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("b"),
Collections.emptyMap(), Collections.emptyMap(), null);
Collections.emptyMap(), Collections.emptyMap(), null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("a")
@ -81,7 +81,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasRenameRenamesMember() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null);
Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("\"z\":\"a\"");
@ -90,7 +90,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasAddAddsMemeber() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null);
Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("\"z\":\"z\"");
@ -103,7 +103,8 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
given(((Instantiator) this.instantiator).instantiateType(TestCustomizer.class)).willReturn(uppercaseCustomizer);
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), Set.of(TestCustomizer.class));
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null,
Set.of(TestCustomizer.class));
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("\"A\":\"a\"");
@ -115,7 +116,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
given(((Instantiator) this.instantiator).instantiateType(FooCustomizer.class)).willReturn(new FooCustomizer());
given(((Instantiator) this.instantiator).instantiateType(BarCustomizer.class)).willReturn(new BarCustomizer());
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(),
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null,
Set.of(FooCustomizer.class, BarCustomizer.class));
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);

View File

@ -16,9 +16,11 @@
package org.springframework.boot.logging.structured;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
@ -26,8 +28,14 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.beans.factory.aot.AotServices;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.StandardStackTracePrinter;
import org.springframework.boot.logging.TestException;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StackTrace;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StackTrace.Root;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -39,16 +47,35 @@ import static org.assertj.core.api.Assertions.assertThat;
class StructuredLoggingJsonPropertiesTests {
@Test
void getBindsFromEnvironment() {
void getWhenHasNoStackTracePropertiesBindsFromEnvironment() {
MockEnvironment environment = new MockEnvironment();
setupJsonProperties(environment);
StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment);
assertThat(properties).isEqualTo(new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("c", "d"),
Map.of("e", "f"), Map.of("g", "h"), null, Set.of(TestCustomizer.class)));
}
@Test
void getWhenHasStackTracePropertiesBindsFromEnvironment() {
MockEnvironment environment = new MockEnvironment();
setupJsonProperties(environment);
environment.setProperty("logging.structured.json.stacktrace.printer", "standard");
environment.setProperty("logging.structured.json.stacktrace.root", "first");
environment.setProperty("logging.structured.json.stacktrace.max-length", "1024");
environment.setProperty("logging.structured.json.stacktrace.max-throwable-depth", "5");
environment.setProperty("logging.structured.json.stacktrace.include-common-frames", "true");
environment.setProperty("logging.structured.json.stacktrace.include-hashes", "true");
StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment);
assertThat(properties.stackTrace())
.isEqualTo(new StructuredLoggingJsonProperties.StackTrace("standard", Root.FIRST, 1024, 5, true, true));
}
private void setupJsonProperties(MockEnvironment environment) {
environment.setProperty("logging.structured.json.include", "a,b");
environment.setProperty("logging.structured.json.exclude", "c,d");
environment.setProperty("logging.structured.json.rename.e", "f");
environment.setProperty("logging.structured.json.add.g", "h");
environment.setProperty("logging.structured.json.customizer", TestCustomizer.class.getName());
StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment);
assertThat(properties).isEqualTo(new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("c", "d"),
Map.of("e", "f"), Map.of("g", "h"), Set.of(TestCustomizer.class)));
}
@Test
@ -64,7 +91,7 @@ class StructuredLoggingJsonPropertiesTests {
assertThat(RuntimeHintsPredicates.reflection().onType(StructuredLoggingJsonProperties.class)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection()
.onConstructor(StructuredLoggingJsonProperties.class.getDeclaredConstructor(Set.class, Set.class, Map.class,
Map.class, Set.class))
Map.class, StackTrace.class, Set.class))
.invoke()).accepts(hints);
}
@ -74,6 +101,90 @@ class StructuredLoggingJsonPropertiesTests {
.anyMatch(StructuredLoggingJsonPropertiesRuntimeHints.class::isInstance);
}
@Nested
class StackTraceTests {
@Test
void createPrinterWhenEmptyReturnsNull() {
StackTrace properties = new StackTrace(null, null, null, null, null, null);
assertThat(properties.createPrinter()).isNull();
}
@Test
void createPrinterWhenNoPrinterAndNotEmptyReturnsStandard() {
StackTrace properties = new StackTrace(null, Root.LAST, null, null, null, null);
assertThat(properties.createPrinter()).isInstanceOf(StandardStackTracePrinter.class);
}
@Test
void createPrinterWhenLoggingSystemReturnsNull() {
StackTrace properties = new StackTrace("logging-system", null, null, null, null, null);
assertThat(properties.createPrinter()).isNull();
}
@Test
void createPrinterWhenLoggingSystemRelaxedReturnsNull() {
StackTrace properties = new StackTrace("LoggingSystem", null, null, null, null, null);
assertThat(properties.createPrinter()).isNull();
}
@Test
void createPrinterWhenStandardReturnsStandardPrinter() {
StackTrace properties = new StackTrace("standard", null, null, null, null, null);
assertThat(properties.createPrinter()).isInstanceOf(StandardStackTracePrinter.class);
}
@Test
void createPrinterWhenStandardRelaxedReturnsStandardPrinter() {
StackTrace properties = new StackTrace("STANDARD", null, null, null, null, null);
assertThat(properties.createPrinter()).isInstanceOf(StandardStackTracePrinter.class);
}
@Test
void createPrinterWhenStandardAppliesCustomizations() {
Exception exception = TestException.create();
StackTrace properties = new StackTrace(null, Root.FIRST, 300, 2, true, false);
StackTracePrinter printer = properties.createPrinter();
String actual = TestException.withoutLineNumbers(printer.printStackTraceToString(exception));
assertThat(actual).isEqualTo("""
java.lang.RuntimeException: exception
at org.springframework.boot.logging.TestException.actualCreateException(TestException.java:NN)
at org.springframework.boot.logging.TestException.createException(TestException.java:NN)
... 2 filtered
Suppressed: java.lang.RuntimeException: supressed
at o...""");
}
@Test
void createPrinterWhenStandardWithHashesPrintsHash() {
Exception exception = TestException.create();
StackTrace properties = new StackTrace(null, null, null, null, null, true);
StackTracePrinter printer = properties.createPrinter();
String actual = printer.printStackTraceToString(exception);
assertThat(actual).containsPattern("<#[0-9a-z]{8}>");
}
@Test
void createPrinterWhenClassNameCreatesPrinter() {
Exception exception = TestException.create();
StackTrace properties = new StackTrace(TestStackTracePrinter.class.getName(), null, null, null, true, null);
StackTracePrinter printer = properties.createPrinter();
assertThat(printer.printStackTraceToString(exception)).isEqualTo("java.lang.RuntimeException: exception");
}
@Test
void createPrinterWhenClassNameInjectsConfiguredPrinter() {
Exception exception = TestException.create();
StackTrace properties = new StackTrace(TestStackTracePrinterCustomized.class.getName(), Root.FIRST, 300, 2,
true, null);
StackTracePrinter printer = properties.createPrinter();
String actual = TestException.withoutLineNumbers(printer.printStackTraceToString(exception));
assertThat(actual).isEqualTo("RuntimeExceptionexception! at org.spr...");
}
}
static class TestCustomizer implements StructuredLoggingJsonMembersCustomizer<String> {
@Override
@ -82,4 +193,30 @@ class StructuredLoggingJsonPropertiesTests {
}
static class TestStackTracePrinter implements StackTracePrinter {
@Override
public void printStackTrace(Throwable throwable, Appendable out) throws IOException {
out.append(throwable.toString());
}
}
static class TestStackTracePrinterCustomized implements StackTracePrinter {
private final StandardStackTracePrinter printer;
TestStackTracePrinterCustomized(StandardStackTracePrinter printer) {
this.printer = printer.withMaximumLength(40)
.withLineSeparator("!")
.withFormatter((throwable) -> ClassUtils.getShortName(throwable.getClass()) + throwable.getMessage());
}
@Override
public void printStackTrace(Throwable throwable, Appendable out) throws IOException {
this.printer.printStackTrace(throwable, out);
}
}
}

View File

@ -76,6 +76,7 @@
<!-- Logging -->
<subpackage name="logging">
<allow pkg="org.springframework.boot.context.properties" />
<allow pkg="org.springframework.boot.context.properties.bind" />
<allow pkg="org.springframework.context.aot" />
<disallow pkg="org.springframework.context" />