Merge branch '3.5.x'

Closes gh-46481
This commit is contained in:
Phillip Webb 2025-07-22 10:54:52 +01:00
commit 4c72ce69da
17 changed files with 231 additions and 48 deletions

View File

@ -84,4 +84,5 @@
<suppress files="SpringBootBanner\.java" checks="SpringLeadingWhitespace" />
<suppress files="LoadTimeWeaverAwareConsumerContainers\.java" checks="InterfaceIsType" />
<suppress files="ConfigurationPropertyCaching\.java" checks="SpringJavadoc" message="\@since"/>
<suppress files="StructuredLoggingJsonMembersCustomizer\.java" checks="SpringJavadoc" message="\@since"/>
</suppressions>

View File

@ -49,8 +49,9 @@ import org.springframework.util.ObjectUtils;
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
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,

View File

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

View File

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

View File

@ -93,10 +93,10 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
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<ILoggingEvent> createGraylogFormatter(Instantiator<?> instantiator) {

View File

@ -28,6 +28,7 @@ import org.springframework.core.env.Environment;
* <ul>
* <li>{@link Environment}</li>
* <li>{@link StructuredLoggingJsonMembersCustomizer}</li>
* <li>{@link StructuredLoggingJsonMembersCustomizer.Builder}</li>
* <li>{@link StackTracePrinter} (may be {@code null})</li>
* <li>{@link ContextPairs}</li>
* </ul>

View File

@ -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<E> {
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<E> {
commonFormatters.accept(this.commonFormatters);
}
StructuredLoggingJsonMembersCustomizer<?> getStructuredLoggingJsonMembersCustomizer(
StructuredLoggingJsonProperties properties) {
List<StructuredLoggingJsonMembersCustomizer<?>> 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<StructuredLoggingJsonMembersCustomizer<?>> loadStructuredLoggingJsonMembersCustomizers() {
return (List) this.factoriesLoader.load(StructuredLoggingJsonMembersCustomizer.class,
ArgumentResolver.from(this.instantiator::getArg));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void invokeCustomizers(List<StructuredLoggingJsonMembersCustomizer<?>> customizers,
Members<Object> 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<E> {
}
/**
* {@link StructuredLoggingJsonMembersCustomizer.Builder} implementation.
*/
class JsonMembersCustomizerBuilder implements StructuredLoggingJsonMembersCustomizer.Builder<E> {
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<E> build() {
return (members) -> {
List<StructuredLoggingJsonMembersCustomizer<?>> 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<StructuredLoggingJsonMembersCustomizer<?>> loadStructuredLoggingJsonMembersCustomizers() {
return (List) StructuredLogFormatterFactory.this.factoriesLoader.load(
StructuredLoggingJsonMembersCustomizer.class,
ArgumentResolver.from(StructuredLogFormatterFactory.this.instantiator::getArg));
}
@SuppressWarnings("unchecked")
private void invokeCustomizers(Members<E> members,
List<StructuredLoggingJsonMembersCustomizer<?>> customizers) {
LambdaSafe.callbacks(StructuredLoggingJsonMembersCustomizer.class, customizers, members)
.withFilter(LambdaSafe.Filter.allowAll())
.invoke((customizer) -> customizer.customize(members));
}
}
}

View File

@ -53,4 +53,37 @@ public interface StructuredLoggingJsonMembersCustomizer<T> {
*/
void customize(JsonWriter.Members<T> members);
/**
* Builder that can be injected into a {@link StructuredLogFormatter} to build the
* {@link StructuredLoggingJsonMembersCustomizer} when specific settings are required.
*
* @param <T> the type being written
* @since 3.5.4
*/
interface Builder<T> {
/**
* Use nested fields when adding JSON from user defined properties.
* @return this builder
*/
default Builder<T> 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<T> nested(boolean nested);
/**
* Build the {@link StructuredLoggingJsonMembersCustomizer}.
* @return the built customizer
*/
StructuredLoggingJsonMembersCustomizer<T> build();
}
}

View File

@ -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<String, String> 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));
}

View File

@ -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<String, Object> map(Object... values) {
assertThat(values.length).isEven();
Map<String, Object> result = new HashMap<>();

View File

@ -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<String, Object> deserialized = deserialize(this.formatter.format(event));

View File

@ -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<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("extra");
assertThat((Map<String, Object>) deserialized.get("extra")).containsEntry("value", "test");
System.out.println(deserialized);
}
@Test
void shouldSupportLogstashCommonFormat() {
StructuredLogLayout layout = newBuilder().setFormat("logstash").build();

View File

@ -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();

View File

@ -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")));

View File

@ -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<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("extra");
assertThat((Map<String, Object>) deserialized.get("extra")).containsEntry("value", "test");
System.out.println(deserialized);
}
@Test
void shouldSupportLogstashCommonFormat() {
this.encoder.setFormat("logstash");

View File

@ -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 <T> the type being written
* @author Phillip Webb
*/
public class MockStructuredLoggingJsonMembersCustomizerBuilder<T>
implements StructuredLoggingJsonMembersCustomizer.Builder<T> {
private final Supplier<StructuredLoggingJsonMembersCustomizer<T>> customizerSupplier;
public MockStructuredLoggingJsonMembersCustomizerBuilder(
Supplier<StructuredLoggingJsonMembersCustomizer<T>> customizerSupplier) {
this.customizerSupplier = customizerSupplier;
}
private boolean nested;
@Override
public Builder<T> nested(boolean nested) {
this.nested = nested;
return this;
}
public boolean isNested() {
return this.nested;
}
@Override
public StructuredLoggingJsonMembersCustomizer<T> build() {
return this.customizerSupplier.get();
}
}

View File

@ -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\"");
}