From b33874a9c9c84054b99f227a57e23e20c088a9c1 Mon Sep 17 00:00:00 2001 From: michaldo Date: Sat, 11 Oct 2025 22:49:59 +0200 Subject: [PATCH] Exclude frames from stacktrace in structured json Signed-off-by: michaldo --- .../StructuredLoggingJsonProperties.java | 23 ++++++-- ...itional-spring-configuration-metadata.json | 5 ++ .../StructuredLoggingJsonPropertiesTests.java | 53 +++++++++++++------ .../reference/pages/features/logging.adoc | 3 ++ 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java index 6ff05dde52f0..2d9f441c6fce 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java @@ -24,7 +24,9 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.UnaryOperator; +import java.util.regex.Pattern; import java.util.stream.Stream; import org.jspecify.annotations.Nullable; @@ -97,10 +99,12 @@ Collection> customizers(Instantia * @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 + * @param excludedFrames list of patterns excluded from stacktrace, f.e. + * java.lang.reflect.Method */ record StackTrace(@Nullable String printer, @Nullable Root root, @Nullable Integer maxLength, - @Nullable Integer maxThrowableDepth, @Nullable Boolean includeCommonFrames, - @Nullable Boolean includeHashes) { + @Nullable Integer maxThrowableDepth, @Nullable Boolean includeCommonFrames, @Nullable Boolean includeHashes, + @Nullable List excludedFrames) { @Nullable StackTracePrinter createPrinter() { String name = sanitizePrinter(); @@ -130,7 +134,8 @@ private String sanitizePrinter() { } private boolean hasAnyOtherProperty() { - return Stream.of(root(), maxLength(), maxThrowableDepth(), includeCommonFrames(), includeHashes()) + return Stream + .of(root(), maxLength(), maxThrowableDepth(), includeCommonFrames(), includeHashes(), excludedFrames()) .anyMatch(Objects::nonNull); } @@ -144,6 +149,10 @@ private StandardStackTracePrinter createStandardPrinter() { printer = map.from(this::includeCommonFrames) .to(printer, apply(StandardStackTracePrinter::withCommonFrames)); printer = map.from(this::includeHashes).to(printer, apply(StandardStackTracePrinter::withHashes)); + printer = map.from(this::excludedFrames) + .whenNot(List::isEmpty) + .as(this::biPredicate) + .to(printer, StandardStackTracePrinter::withFrameFilter); return printer; } @@ -152,6 +161,14 @@ private BiFunction (!value) ? printer : action.apply(printer); } + private BiPredicate biPredicate(List excludedFrames) { + List exclusionPatterns = excludedFrames.stream().map(Pattern::compile).toList(); + return (ignored, element) -> { + String classNameAndMethod = element.getClassName() + "." + element.getMethodName(); + return exclusionPatterns.stream().noneMatch((pattern) -> pattern.matcher(classNameAndMethod).find()); + }; + } + /** * Root ordering. */ diff --git a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 26d63493f927..74fc53f1fcab 100644 --- a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -302,6 +302,11 @@ "type": "java.util.Map", "description": "Mapping between member paths and an alternative name that should be used in structured logging JSON" }, + { + "name": "logging.structured.json.stacktrace.excluded-frames", + "type": "java.util.List", + "description": "List of patterns excluded from stacktrace, f.e. java.lang.reflect.Method." + }, { "name": "logging.structured.json.stacktrace.include-common-frames", "type": "java.lang.Boolean", diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java index 54bca909f750..8b322ce17dcd 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java @@ -17,11 +17,14 @@ package org.springframework.boot.logging.structured; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -66,10 +69,11 @@ void getWhenHasStackTracePropertiesBindsFromEnvironment() { 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"); + environment.setProperty("logging.structured.json.stacktrace.excluded-frames", "java.lang.reflect.Method"); StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment); assertThat(properties).isNotNull(); - assertThat(properties.stackTrace()) - .isEqualTo(new StructuredLoggingJsonProperties.StackTrace("standard", Root.FIRST, 1024, 5, true, true)); + assertThat(properties.stackTrace()).isEqualTo(new StructuredLoggingJsonProperties.StackTrace("standard", + Root.FIRST, 1024, 5, true, true, List.of("java.lang.reflect.Method"))); } private void setupJsonProperties(MockEnvironment environment) { @@ -98,7 +102,7 @@ void shouldRegisterRuntimeHints() throws Exception { .accepts(hints); assertThat(RuntimeHintsPredicates.reflection() .onConstructorInvocation(StackTrace.class.getDeclaredConstructor(String.class, Root.class, Integer.class, - Integer.class, Boolean.class, Boolean.class))) + Integer.class, Boolean.class, Boolean.class, List.class))) .accepts(hints); assertThat(RuntimeHintsPredicates.reflection() .onConstructorInvocation(Context.class.getDeclaredConstructor(boolean.class, String.class))).accepts(hints); @@ -115,44 +119,44 @@ class StackTraceTests { @Test void createPrinterWhenEmptyReturnsNull() { - StackTrace properties = new StackTrace(null, null, null, null, null, null); + StackTrace properties = new StackTrace(null, null, null, null, null, null, null); assertThat(properties.createPrinter()).isNull(); } @Test void createPrinterWhenNoPrinterAndNotEmptyReturnsStandard() { - StackTrace properties = new StackTrace(null, Root.LAST, null, null, null, null); + StackTrace properties = new StackTrace(null, Root.LAST, null, null, null, null, null); assertThat(properties.createPrinter()).isInstanceOf(StandardStackTracePrinter.class); } @Test void createPrinterWhenLoggingSystemReturnsNull() { - StackTrace properties = new StackTrace("logging-system", null, null, null, null, null); + StackTrace properties = new StackTrace("logging-system", null, null, null, null, null, null); assertThat(properties.createPrinter()).isNull(); } @Test void createPrinterWhenLoggingSystemRelaxedReturnsNull() { - StackTrace properties = new StackTrace("LoggingSystem", null, null, null, null, null); + StackTrace properties = new StackTrace("LoggingSystem", null, null, null, null, null, null); assertThat(properties.createPrinter()).isNull(); } @Test void createPrinterWhenStandardReturnsStandardPrinter() { - StackTrace properties = new StackTrace("standard", null, null, null, null, null); + StackTrace properties = new StackTrace("standard", null, null, null, null, null, null); assertThat(properties.createPrinter()).isInstanceOf(StandardStackTracePrinter.class); } @Test void createPrinterWhenStandardRelaxedReturnsStandardPrinter() { - StackTrace properties = new StackTrace("STANDARD", null, null, null, null, null); + StackTrace properties = new StackTrace("STANDARD", null, 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); + StackTrace properties = new StackTrace(null, Root.FIRST, 300, 2, true, false, null); StandardStackTracePrinter printer = (StandardStackTracePrinter) properties.createPrinter(); assertThat(printer).isNotNull(); printer = printer.withLineSeparator("\n"); @@ -168,17 +172,32 @@ void createPrinterWhenStandardAppliesCustomizations() { @Test void createPrinterWhenStandardWithHashesPrintsHash() { Exception exception = TestException.create(); - StackTrace properties = new StackTrace(null, null, null, null, null, true); + StackTrace properties = new StackTrace(null, null, null, null, null, true, null); StackTracePrinter printer = properties.createPrinter(); assertThat(printer).isNotNull(); String actual = printer.printStackTraceToString(exception); assertThat(actual).containsPattern("<#[0-9a-z]{8}>"); } + @ParameterizedTest + @CsvSource({ + "org.springframework.boot.logging.TestException.createTestException, org.springframework.boot.logging.TestException.createException, org.springframework.boot.logging.TestException.createTestException", + "org.springframework.boot.logging.TestException.createException, org.springframework.boot.logging.TestException.createTestException, org.springframework.boot.logging.TestException.createException" }) + void createPrinterWhenStandardWithExcludedFramesSuppressFrames(String excludedFrame, String present, + String absent) { + Exception exception = TestException.create(); + StackTrace properties = new StackTrace(null, null, null, null, null, null, List.of(excludedFrame)); + StackTracePrinter printer = properties.createPrinter(); + assertThat(printer).isNotNull(); + String actual = printer.printStackTraceToString(exception); + assertThat(actual).containsPattern(present).doesNotContain(absent).contains("filtered"); + } + @Test void createPrinterWhenClassNameCreatesPrinter() { Exception exception = TestException.create(); - StackTrace properties = new StackTrace(TestStackTracePrinter.class.getName(), null, null, null, true, null); + StackTrace properties = new StackTrace(TestStackTracePrinter.class.getName(), null, null, null, true, null, + null); StackTracePrinter printer = properties.createPrinter(); assertThat(printer).isNotNull(); assertThat(printer.printStackTraceToString(exception)).isEqualTo("java.lang.RuntimeException: exception"); @@ -188,7 +207,7 @@ void createPrinterWhenClassNameCreatesPrinter() { void createPrinterWhenClassNameInjectsConfiguredPrinter() { Exception exception = TestException.create(); StackTrace properties = new StackTrace(TestStackTracePrinterCustomized.class.getName(), Root.FIRST, 300, 2, - true, null); + true, null, null); StackTracePrinter printer = properties.createPrinter(); assertThat(printer).isNotNull(); String actual = TestException.withoutLineNumbers(printer.printStackTraceToString(exception)); @@ -197,25 +216,25 @@ void createPrinterWhenClassNameInjectsConfiguredPrinter() { @Test void hasCustomPrinterShouldReturnFalseWhenPrinterIsEmpty() { - StackTrace stackTrace = new StackTrace("", null, null, null, null, null); + StackTrace stackTrace = new StackTrace("", null, null, null, null, null, null); assertThat(stackTrace.hasCustomPrinter()).isFalse(); } @Test void hasCustomPrinterShouldReturnFalseWhenPrinterHasLoggingSystem() { - StackTrace stackTrace = new StackTrace("loggingsystem", null, null, null, null, null); + StackTrace stackTrace = new StackTrace("loggingsystem", null, null, null, null, null, null); assertThat(stackTrace.hasCustomPrinter()).isFalse(); } @Test void hasCustomPrinterShouldReturnFalseWhenPrinterHasStandard() { - StackTrace stackTrace = new StackTrace("standard", null, null, null, null, null); + StackTrace stackTrace = new StackTrace("standard", null, null, null, null, null, null); assertThat(stackTrace.hasCustomPrinter()).isFalse(); } @Test void hasCustomPrinterShouldReturnTrueWhenPrinterHasCustom() { - StackTrace stackTrace = new StackTrace("custom-printer", null, null, null, null, null); + StackTrace stackTrace = new StackTrace("custom-printer", null, null, null, null, null, null); assertThat(stackTrace.hasCustomPrinter()).isTrue(); } diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc index cf0e156f63e5..9be8f5149c4f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc @@ -674,6 +674,9 @@ To do this, you can use one or more of the following properties: | configprop:logging.structured.json.stacktrace.include-hashes[] | If a hash of the stack trace should be included + +| configprop:logging.structured.json.stacktrace.exluded-frames[] +| List of patterns excluded from stacktrace, f.e. `java.lang.reflect.Method`, `ByCGLIB` |=== For example, the following will use root first stack traces, limit their length, and include hashes.