From b0350a52c2f139578502bf09ff04ce85dd82d7c6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 22 Jul 2025 10:52:27 +0100 Subject: [PATCH] Add additional ECS fields using nested structure Update ECS structured logging so that additional fields are written using a nested structure. A new `Builder` interface has been added to `StructuredLoggingJsonMembersCustomizer` which can be injected into formatters and allows `nested` settings to be specified. Fixes gh-46351 --- ...ticCommonSchemaStructuredLogFormatter.java | 5 +- .../logging/log4j2/StructuredLogLayout.java | 6 +- ...ticCommonSchemaStructuredLogFormatter.java | 4 +- .../logging/logback/StructuredLogEncoder.java | 6 +- .../structured/StructuredLogFormatter.java | 1 + .../StructuredLogFormatterFactory.java | 78 +++++++++++++------ ...tructuredLoggingJsonMembersCustomizer.java | 33 ++++++++ ...ngJsonPropertiesJsonMembersCustomizer.java | 13 +++- .../AbstractStructuredLoggingTests.java | 4 + ...mmonSchemaStructuredLogFormatterTests.java | 9 ++- .../log4j2/StructuredLogLayoutTests.java | 12 +++ .../AbstractStructuredLoggingTests.java | 4 + ...mmonSchemaStructuredLogFormatterTests.java | 9 ++- .../logback/StructuredLogEncoderTests.java | 15 ++++ ...edLoggingJsonMembersCustomizerBuilder.java | 56 +++++++++++++ ...nPropertiesJsonMembersCustomizerTests.java | 23 ++++-- src/checkstyle/checkstyle-suppressions.xml | 1 + 17 files changed, 231 insertions(+), 48 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/MockStructuredLoggingJsonMembersCustomizerBuilder.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java index 8989ebcf3a1..1e9b7fcd59b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java @@ -49,8 +49,9 @@ import org.springframework.util.ObjectUtils; class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter { ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter, - ContextPairs contextPairs, StructuredLoggingJsonMembersCustomizer customizer) { - super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, members), customizer); + ContextPairs contextPairs, StructuredLoggingJsonMembersCustomizer.Builder customizerBuilder) { + super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, members), + customizerBuilder.nested().build()); } private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter, diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java index eff5894ba7c..ab94016d5c0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java @@ -115,10 +115,10 @@ final class StructuredLogLayout extends AbstractStringLayout { Environment environment = instantiator.getArg(Environment.class); StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class); ContextPairs contextPairs = instantiator.getArg(ContextPairs.class); - StructuredLoggingJsonMembersCustomizer jsonMembersCustomizer = instantiator - .getArg(StructuredLoggingJsonMembersCustomizer.class); + StructuredLoggingJsonMembersCustomizer.Builder jsonMembersCustomizerBuilder = instantiator + .getArg(StructuredLoggingJsonMembersCustomizer.Builder.class); return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, contextPairs, - jsonMembersCustomizer); + jsonMembersCustomizerBuilder); } private GraylogExtendedLogFormatStructuredLogFormatter createGraylogFormatter(Instantiator instantiator) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java index 61f62933a9a..f952b1518e2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java @@ -53,9 +53,9 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter, ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter, - StructuredLoggingJsonMembersCustomizer customizer) { + StructuredLoggingJsonMembersCustomizer.Builder customizerBuilder) { super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, throwableProxyConverter, members), - customizer); + customizerBuilder.nested().build()); } private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter, diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java index a78e835a52d..3524be0c102 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java @@ -93,10 +93,10 @@ public class StructuredLogEncoder extends EncoderBase { StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class); ContextPairs contextParis = instantiator.getArg(ContextPairs.class); ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class); - StructuredLoggingJsonMembersCustomizer jsonMembersCustomizer = instantiator - .getArg(StructuredLoggingJsonMembersCustomizer.class); + StructuredLoggingJsonMembersCustomizer.Builder jsonMembersCustomizerBuilder = instantiator + .getArg(StructuredLoggingJsonMembersCustomizer.Builder.class); return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, contextParis, - throwableProxyConverter, jsonMembersCustomizer); + throwableProxyConverter, jsonMembersCustomizerBuilder); } private StructuredLogFormatter createGraylogFormatter(Instantiator instantiator) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java index d8c7849508b..a17fff728d8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java @@ -30,6 +30,7 @@ import org.springframework.core.env.Environment; *
    *
  • {@link Environment}
  • *
  • {@link StructuredLoggingJsonMembersCustomizer}
  • + *
  • {@link StructuredLoggingJsonMembersCustomizer.Builder}
  • *
  • {@link StackTracePrinter} (may be {@code null})
  • *
  • {@link ContextPairs}
  • *
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java index 18c95a463fd..3d56c441c07 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java @@ -29,6 +29,7 @@ import org.springframework.boot.logging.structured.StructuredLoggingJsonProperti import org.springframework.boot.util.Instantiator; import org.springframework.boot.util.Instantiator.AvailableParameters; import org.springframework.boot.util.Instantiator.FailureHandler; +import org.springframework.boot.util.LambdaSafe; import org.springframework.core.GenericTypeResolver; import org.springframework.core.env.Environment; import org.springframework.core.io.support.SpringFactoriesLoader; @@ -85,7 +86,9 @@ public class StructuredLogFormatterFactory { this.instantiator = new Instantiator<>(Object.class, (allAvailableParameters) -> { allAvailableParameters.add(Environment.class, environment); allAvailableParameters.add(StructuredLoggingJsonMembersCustomizer.class, - (type) -> getStructuredLoggingJsonMembersCustomizer(properties)); + new JsonMembersCustomizerBuilder(properties).build()); + allAvailableParameters.add(StructuredLoggingJsonMembersCustomizer.Builder.class, + new JsonMembersCustomizerBuilder(properties)); allAvailableParameters.add(StackTracePrinter.class, (type) -> getStackTracePrinter(properties)); allAvailableParameters.add(ContextPairs.class, (type) -> getContextPairs(properties)); if (availableParameters != null) { @@ -96,30 +99,6 @@ public class StructuredLogFormatterFactory { commonFormatters.accept(this.commonFormatters); } - StructuredLoggingJsonMembersCustomizer getStructuredLoggingJsonMembersCustomizer( - StructuredLoggingJsonProperties properties) { - List> customizers = new ArrayList<>(); - if (properties != null) { - customizers.add(new StructuredLoggingJsonPropertiesJsonMembersCustomizer(this.instantiator, properties)); - } - customizers.addAll(loadStructuredLoggingJsonMembersCustomizers()); - return (members) -> invokeCustomizers(customizers, members); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private List> loadStructuredLoggingJsonMembersCustomizers() { - return (List) this.factoriesLoader.load(StructuredLoggingJsonMembersCustomizer.class, - ArgumentResolver.from(this.instantiator::getArg)); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private void invokeCustomizers(List> customizers, - Members members) { - for (StructuredLoggingJsonMembersCustomizer customizer : customizers) { - ((StructuredLoggingJsonMembersCustomizer) customizer).customize(members); - } - } - private StackTracePrinter getStackTracePrinter(StructuredLoggingJsonProperties properties) { return (properties != null && properties.stackTrace() != null) ? properties.stackTrace().createPrinter() : null; } @@ -218,4 +197,53 @@ public class StructuredLogFormatterFactory { } + /** + * {@link StructuredLoggingJsonMembersCustomizer.Builder} implementation. + */ + class JsonMembersCustomizerBuilder implements StructuredLoggingJsonMembersCustomizer.Builder { + + private final StructuredLoggingJsonProperties properties; + + private boolean nested; + + JsonMembersCustomizerBuilder(StructuredLoggingJsonProperties properties) { + this.properties = properties; + } + + @Override + public JsonMembersCustomizerBuilder nested(boolean nested) { + this.nested = nested; + return this; + } + + @Override + public StructuredLoggingJsonMembersCustomizer build() { + return (members) -> { + List> customizers = new ArrayList<>(); + if (this.properties != null) { + customizers.add(new StructuredLoggingJsonPropertiesJsonMembersCustomizer( + StructuredLogFormatterFactory.this.instantiator, this.properties, this.nested)); + } + customizers.addAll(loadStructuredLoggingJsonMembersCustomizers()); + invokeCustomizers(members, customizers); + }; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private List> loadStructuredLoggingJsonMembersCustomizers() { + return (List) StructuredLogFormatterFactory.this.factoriesLoader.load( + StructuredLoggingJsonMembersCustomizer.class, + ArgumentResolver.from(StructuredLogFormatterFactory.this.instantiator::getArg)); + } + + @SuppressWarnings("unchecked") + private void invokeCustomizers(Members members, + List> customizers) { + LambdaSafe.callbacks(StructuredLoggingJsonMembersCustomizer.class, customizers, members) + .withFilter(LambdaSafe.Filter.allowAll()) + .invoke((customizer) -> customizer.customize(members)); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizer.java index 69c3bee1ee2..f230f2fd6fc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizer.java @@ -55,4 +55,37 @@ public interface StructuredLoggingJsonMembersCustomizer { */ void customize(JsonWriter.Members members); + /** + * Builder that can be injected into a {@link StructuredLogFormatter} to build the + * {@link StructuredLoggingJsonMembersCustomizer} when specific settings are required. + * + * @param the type being written + * @since 3.5.4 + */ + interface Builder { + + /** + * Use nested fields when adding JSON from user defined properties. + * @return this builder + */ + default Builder nested() { + return nested(true); + } + + /** + * Set if nested fields should be used when adding JSON from user defined + * properties. + * @param nested if nested fields are to be used + * @return this builder + */ + Builder nested(boolean nested); + + /** + * Build the {@link StructuredLoggingJsonMembersCustomizer}. + * @return the built customizer + */ + StructuredLoggingJsonMembersCustomizer build(); + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java index 44c47b83a6e..c14afccd732 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java @@ -36,10 +36,13 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizer implements Structured private final StructuredLoggingJsonProperties properties; + private final boolean nested; + StructuredLoggingJsonPropertiesJsonMembersCustomizer(Instantiator instantiator, - StructuredLoggingJsonProperties properties) { + StructuredLoggingJsonProperties properties, boolean nested) { this.instantiator = instantiator; this.properties = properties; + this.nested = nested; } @Override @@ -48,7 +51,13 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizer implements Structured members.applyingNameProcessor(this::renameJsonMembers); Map add = this.properties.add(); if (!CollectionUtils.isEmpty(add)) { - add.forEach(members::add); + if (this.nested) { + ContextPairs contextPairs = new ContextPairs(true, ""); + members.add().usingPairs(contextPairs.nested((pairs) -> pairs.addMapEntries((source) -> add))); + } + else { + add.forEach(members::add); + } } this.properties.customizers(this.instantiator).forEach((customizer) -> customizer.customize(members)); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/AbstractStructuredLoggingTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/AbstractStructuredLoggingTests.java index f9c9b2318cc..da4c590e247 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/AbstractStructuredLoggingTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/AbstractStructuredLoggingTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.logging.structured.MockStructuredLoggingJsonMembersCustomizerBuilder; import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer; import static org.assertj.core.api.Assertions.assertThat; @@ -50,6 +51,9 @@ abstract class AbstractStructuredLoggingTests { @Mock StructuredLoggingJsonMembersCustomizer customizer; + MockStructuredLoggingJsonMembersCustomizerBuilder customizerBuilder = new MockStructuredLoggingJsonMembersCustomizerBuilder<>( + () -> this.customizer); + protected Map map(Object... values) { assertThat(values.length).isEven(); Map result = new HashMap<>(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java index fb1a2a6c284..9a63f224913 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java @@ -56,7 +56,12 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL 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, - TestContextPairs.include(), this.customizer); + TestContextPairs.include(), this.customizerBuilder); + } + + @Test + void callsNestedOnCustomizerBuilder() { + assertThat(this.customizerBuilder.isNested()).isTrue(); } @Test @@ -109,7 +114,7 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL @SuppressWarnings("unchecked") void shouldFormatExceptionUsingStackTracePrinter() { this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(), - TestContextPairs.include(), this.customizer); + TestContextPairs.include(), this.customizerBuilder); MutableLogEvent event = createEvent(); event.setThrown(new RuntimeException("Boom")); Map deserialized = deserialize(this.formatter.format(event)); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/StructuredLogLayoutTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/StructuredLogLayoutTests.java index 7d4448af8bb..f156218d28e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/StructuredLogLayoutTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/StructuredLogLayoutTests.java @@ -72,6 +72,18 @@ class StructuredLogLayoutTests extends AbstractStructuredLoggingTests { assertThat(error.get("stack_trace")).isEqualTo("stacktrace:RuntimeException"); } + @Test + @SuppressWarnings("unchecked") + void shouldOutputNestedAdditionalEcsJson() { + this.environment.setProperty("logging.structured.json.add.extra.value", "test"); + StructuredLogLayout layout = newBuilder().setFormat("ecs").build(); + String json = layout.toSerializable(createEvent(new RuntimeException("Boom!"))); + Map deserialized = deserialize(json); + assertThat(deserialized).containsKey("extra"); + assertThat((Map) deserialized.get("extra")).containsEntry("value", "test"); + System.out.println(deserialized); + } + @Test void shouldSupportLogstashCommonFormat() { StructuredLogLayout layout = newBuilder().setFormat("logstash").build(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/AbstractStructuredLoggingTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/AbstractStructuredLoggingTests.java index 602c22f38b7..e6f97e6b7d5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/AbstractStructuredLoggingTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/AbstractStructuredLoggingTests.java @@ -39,6 +39,7 @@ import org.slf4j.Marker; import org.slf4j.event.KeyValuePair; import org.slf4j.helpers.BasicMarkerFactory; +import org.springframework.boot.logging.structured.MockStructuredLoggingJsonMembersCustomizerBuilder; import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer; import static org.assertj.core.api.Assertions.assertThat; @@ -62,6 +63,9 @@ abstract class AbstractStructuredLoggingTests { @Mock StructuredLoggingJsonMembersCustomizer customizer; + MockStructuredLoggingJsonMembersCustomizerBuilder customizerBuilder = new MockStructuredLoggingJsonMembersCustomizerBuilder<>( + () -> this.customizer); + @BeforeEach void setUp() { this.markerFactory = new BasicMarkerFactory(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java index 6e3fd7aac6a..7ddb4b5cc47 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java @@ -58,7 +58,12 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL 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, - TestContextPairs.include(), getThrowableProxyConverter(), this.customizer); + TestContextPairs.include(), getThrowableProxyConverter(), this.customizerBuilder); + } + + @Test + void callsNestedOnCustomizerBuilder() { + assertThat(this.customizerBuilder.isNested()).isTrue(); } @Test @@ -115,7 +120,7 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL @SuppressWarnings("unchecked") void shouldFormatExceptionUsingStackTracePrinter() { this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(), - TestContextPairs.include(), getThrowableProxyConverter(), this.customizer); + TestContextPairs.include(), getThrowableProxyConverter(), this.customizerBuilder); LoggingEvent event = createEvent(); event.setMDCPropertyMap(Collections.emptyMap()); event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom"))); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/StructuredLogEncoderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/StructuredLogEncoderTests.java index 5cc15b2f2fe..986f7bf3e6a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/StructuredLogEncoderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/StructuredLogEncoderTests.java @@ -85,6 +85,21 @@ class StructuredLogEncoderTests extends AbstractStructuredLoggingTests { assertThat(error.get("stack_trace")).isEqualTo("stacktrace:RuntimeException"); } + @Test + @SuppressWarnings("unchecked") + void shouldOutputNestedAdditionalEcsJson() { + this.environment.setProperty("logging.structured.json.add.extra.value", "test"); + this.encoder.setFormat("ecs"); + this.encoder.start(); + LoggingEvent event = createEvent(); + event.setMDCPropertyMap(Collections.emptyMap()); + String json = encode(event); + Map deserialized = deserialize(json); + assertThat(deserialized).containsKey("extra"); + assertThat((Map) deserialized.get("extra")).containsEntry("value", "test"); + System.out.println(deserialized); + } + @Test void shouldSupportLogstashCommonFormat() { this.encoder.setFormat("logstash"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/MockStructuredLoggingJsonMembersCustomizerBuilder.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/MockStructuredLoggingJsonMembersCustomizerBuilder.java new file mode 100644 index 00000000000..c405b2bee60 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/MockStructuredLoggingJsonMembersCustomizerBuilder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-present 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.structured; + +import java.util.function.Supplier; + +import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer.Builder; + +/** + * Mock {@link StructuredLoggingJsonMembersCustomizer.Builder}. + * + * @param the type being written + * @author Phillip Webb + */ +public class MockStructuredLoggingJsonMembersCustomizerBuilder + implements StructuredLoggingJsonMembersCustomizer.Builder { + + private final Supplier> customizerSupplier; + + public MockStructuredLoggingJsonMembersCustomizerBuilder( + Supplier> customizerSupplier) { + this.customizerSupplier = customizerSupplier; + } + + private boolean nested; + + @Override + public Builder nested(boolean nested) { + this.nested = nested; + return this; + } + + public boolean isNested() { + return this.nested; + } + + @Override + public StructuredLoggingJsonMembersCustomizer build() { + return this.customizerSupplier.get(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java index a2cef2453bf..1441025aef7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java @@ -51,7 +51,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null, null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( - this.instantiator, properties); + this.instantiator, properties, false); assertThat(writeSampleJson(customizer)).doesNotContain("a").contains("b"); } @@ -60,7 +60,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a"), Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( - this.instantiator, properties); + this.instantiator, properties, false); assertThat(writeSampleJson(customizer)).contains("a") .doesNotContain("b") .doesNotContain("c") @@ -72,7 +72,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("b"), Collections.emptyMap(), Collections.emptyMap(), null, null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( - this.instantiator, properties); + this.instantiator, properties, false); assertThat(writeSampleJson(customizer)).contains("a") .doesNotContain("b") .doesNotContain("c") @@ -84,7 +84,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null, null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( - this.instantiator, properties); + this.instantiator, properties, false); assertThat(writeSampleJson(customizer)).contains("\"z\":\"a\""); } @@ -93,10 +93,19 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null, null, null); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( - this.instantiator, properties); + this.instantiator, properties, false); assertThat(writeSampleJson(customizer)).contains("\"z\":\"z\""); } + @Test + void customizeWhenHasNestedAddAddsMember() { + StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(), + Collections.emptySet(), Collections.emptyMap(), Map.of("y.z", "yz"), null, null, null); + StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( + this.instantiator, properties, true); + assertThat(writeSampleJson(customizer)).contains("\"y\":{\"z\":\"yz\"}"); + } + @Test @SuppressWarnings("unchecked") void customizeWhenHasCustomizerCustomizesMember() { @@ -107,7 +116,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null, Set.of(TestCustomizer.class)); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( - this.instantiator, properties); + this.instantiator, properties, false); assertThat(writeSampleJson(customizer)).contains("\"A\":\"a\""); } @@ -120,7 +129,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests { Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null, Set.of(FooCustomizer.class, BarCustomizer.class)); StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer( - this.instantiator, properties); + this.instantiator, properties, false); assertThat(writeSampleJson(customizer)).contains("\"foo\":\"foo\"").contains("\"bar\":\"bar\""); } diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 1cdb218d5a9..d35145ac1c0 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -86,4 +86,5 @@ +