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:
parent
291e5d8bd3
commit
7433b93769
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user