Skip to content

Commit b0350a5

Browse files
committed
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
1 parent 8a3b81e commit b0350a5

17 files changed

+231
-48
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@
4949
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
5050

5151
ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
52-
ContextPairs contextPairs, StructuredLoggingJsonMembersCustomizer<?> customizer) {
53-
super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, members), customizer);
52+
ContextPairs contextPairs, StructuredLoggingJsonMembersCustomizer.Builder<?> customizerBuilder) {
53+
super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, members),
54+
customizerBuilder.nested().build());
5455
}
5556

5657
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ private ElasticCommonSchemaStructuredLogFormatter createEcsFormatter(Instantiato
115115
Environment environment = instantiator.getArg(Environment.class);
116116
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
117117
ContextPairs contextPairs = instantiator.getArg(ContextPairs.class);
118-
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
119-
.getArg(StructuredLoggingJsonMembersCustomizer.class);
118+
StructuredLoggingJsonMembersCustomizer.Builder<?> jsonMembersCustomizerBuilder = instantiator
119+
.getArg(StructuredLoggingJsonMembersCustomizer.Builder.class);
120120
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, contextPairs,
121-
jsonMembersCustomizer);
121+
jsonMembersCustomizerBuilder);
122122
}
123123

124124
private GraylogExtendedLogFormatStructuredLogFormatter createGraylogFormatter(Instantiator<?> instantiator) {

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
5353

5454
ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
5555
ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter,
56-
StructuredLoggingJsonMembersCustomizer<?> customizer) {
56+
StructuredLoggingJsonMembersCustomizer.Builder<?> customizerBuilder) {
5757
super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, throwableProxyConverter, members),
58-
customizer);
58+
customizerBuilder.nested().build());
5959
}
6060

6161
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ private StructuredLogFormatter<ILoggingEvent> createEcsFormatter(Instantiator<?>
9393
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
9494
ContextPairs contextParis = instantiator.getArg(ContextPairs.class);
9595
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
96-
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
97-
.getArg(StructuredLoggingJsonMembersCustomizer.class);
96+
StructuredLoggingJsonMembersCustomizer.Builder<?> jsonMembersCustomizerBuilder = instantiator
97+
.getArg(StructuredLoggingJsonMembersCustomizer.Builder.class);
9898
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, contextParis,
99-
throwableProxyConverter, jsonMembersCustomizer);
99+
throwableProxyConverter, jsonMembersCustomizerBuilder);
100100
}
101101

102102
private StructuredLogFormatter<ILoggingEvent> createGraylogFormatter(Instantiator<?> instantiator) {

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* <ul>
3131
* <li>{@link Environment}</li>
3232
* <li>{@link StructuredLoggingJsonMembersCustomizer}</li>
33+
* <li>{@link StructuredLoggingJsonMembersCustomizer.Builder}</li>
3334
* <li>{@link StackTracePrinter} (may be {@code null})</li>
3435
* <li>{@link ContextPairs}</li>
3536
* </ul>

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.boot.util.Instantiator;
3030
import org.springframework.boot.util.Instantiator.AvailableParameters;
3131
import org.springframework.boot.util.Instantiator.FailureHandler;
32+
import org.springframework.boot.util.LambdaSafe;
3233
import org.springframework.core.GenericTypeResolver;
3334
import org.springframework.core.env.Environment;
3435
import org.springframework.core.io.support.SpringFactoriesLoader;
@@ -85,7 +86,9 @@ public StructuredLogFormatterFactory(Class<E> logEventType, Environment environm
8586
this.instantiator = new Instantiator<>(Object.class, (allAvailableParameters) -> {
8687
allAvailableParameters.add(Environment.class, environment);
8788
allAvailableParameters.add(StructuredLoggingJsonMembersCustomizer.class,
88-
(type) -> getStructuredLoggingJsonMembersCustomizer(properties));
89+
new JsonMembersCustomizerBuilder(properties).build());
90+
allAvailableParameters.add(StructuredLoggingJsonMembersCustomizer.Builder.class,
91+
new JsonMembersCustomizerBuilder(properties));
8992
allAvailableParameters.add(StackTracePrinter.class, (type) -> getStackTracePrinter(properties));
9093
allAvailableParameters.add(ContextPairs.class, (type) -> getContextPairs(properties));
9194
if (availableParameters != null) {
@@ -96,30 +99,6 @@ public StructuredLogFormatterFactory(Class<E> logEventType, Environment environm
9699
commonFormatters.accept(this.commonFormatters);
97100
}
98101

99-
StructuredLoggingJsonMembersCustomizer<?> getStructuredLoggingJsonMembersCustomizer(
100-
StructuredLoggingJsonProperties properties) {
101-
List<StructuredLoggingJsonMembersCustomizer<?>> customizers = new ArrayList<>();
102-
if (properties != null) {
103-
customizers.add(new StructuredLoggingJsonPropertiesJsonMembersCustomizer(this.instantiator, properties));
104-
}
105-
customizers.addAll(loadStructuredLoggingJsonMembersCustomizers());
106-
return (members) -> invokeCustomizers(customizers, members);
107-
}
108-
109-
@SuppressWarnings({ "unchecked", "rawtypes" })
110-
private List<StructuredLoggingJsonMembersCustomizer<?>> loadStructuredLoggingJsonMembersCustomizers() {
111-
return (List) this.factoriesLoader.load(StructuredLoggingJsonMembersCustomizer.class,
112-
ArgumentResolver.from(this.instantiator::getArg));
113-
}
114-
115-
@SuppressWarnings({ "unchecked", "rawtypes" })
116-
private void invokeCustomizers(List<StructuredLoggingJsonMembersCustomizer<?>> customizers,
117-
Members<Object> members) {
118-
for (StructuredLoggingJsonMembersCustomizer<?> customizer : customizers) {
119-
((StructuredLoggingJsonMembersCustomizer) customizer).customize(members);
120-
}
121-
}
122-
123102
private StackTracePrinter getStackTracePrinter(StructuredLoggingJsonProperties properties) {
124103
return (properties != null && properties.stackTrace() != null) ? properties.stackTrace().createPrinter() : null;
125104
}
@@ -218,4 +197,53 @@ public interface CommonFormatterFactory<E> {
218197

219198
}
220199

200+
/**
201+
* {@link StructuredLoggingJsonMembersCustomizer.Builder} implementation.
202+
*/
203+
class JsonMembersCustomizerBuilder implements StructuredLoggingJsonMembersCustomizer.Builder<E> {
204+
205+
private final StructuredLoggingJsonProperties properties;
206+
207+
private boolean nested;
208+
209+
JsonMembersCustomizerBuilder(StructuredLoggingJsonProperties properties) {
210+
this.properties = properties;
211+
}
212+
213+
@Override
214+
public JsonMembersCustomizerBuilder nested(boolean nested) {
215+
this.nested = nested;
216+
return this;
217+
}
218+
219+
@Override
220+
public StructuredLoggingJsonMembersCustomizer<E> build() {
221+
return (members) -> {
222+
List<StructuredLoggingJsonMembersCustomizer<?>> customizers = new ArrayList<>();
223+
if (this.properties != null) {
224+
customizers.add(new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
225+
StructuredLogFormatterFactory.this.instantiator, this.properties, this.nested));
226+
}
227+
customizers.addAll(loadStructuredLoggingJsonMembersCustomizers());
228+
invokeCustomizers(members, customizers);
229+
};
230+
}
231+
232+
@SuppressWarnings({ "unchecked", "rawtypes" })
233+
private List<StructuredLoggingJsonMembersCustomizer<?>> loadStructuredLoggingJsonMembersCustomizers() {
234+
return (List) StructuredLogFormatterFactory.this.factoriesLoader.load(
235+
StructuredLoggingJsonMembersCustomizer.class,
236+
ArgumentResolver.from(StructuredLogFormatterFactory.this.instantiator::getArg));
237+
}
238+
239+
@SuppressWarnings("unchecked")
240+
private void invokeCustomizers(Members<E> members,
241+
List<StructuredLoggingJsonMembersCustomizer<?>> customizers) {
242+
LambdaSafe.callbacks(StructuredLoggingJsonMembersCustomizer.class, customizers, members)
243+
.withFilter(LambdaSafe.Filter.allowAll())
244+
.invoke((customizer) -> customizer.customize(members));
245+
}
246+
247+
}
248+
221249
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizer.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,37 @@ public interface StructuredLoggingJsonMembersCustomizer<T> {
5555
*/
5656
void customize(JsonWriter.Members<T> members);
5757

58+
/**
59+
* Builder that can be injected into a {@link StructuredLogFormatter} to build the
60+
* {@link StructuredLoggingJsonMembersCustomizer} when specific settings are required.
61+
*
62+
* @param <T> the type being written
63+
* @since 3.5.4
64+
*/
65+
interface Builder<T> {
66+
67+
/**
68+
* Use nested fields when adding JSON from user defined properties.
69+
* @return this builder
70+
*/
71+
default Builder<T> nested() {
72+
return nested(true);
73+
}
74+
75+
/**
76+
* Set if nested fields should be used when adding JSON from user defined
77+
* properties.
78+
* @param nested if nested fields are to be used
79+
* @return this builder
80+
*/
81+
Builder<T> nested(boolean nested);
82+
83+
/**
84+
* Build the {@link StructuredLoggingJsonMembersCustomizer}.
85+
* @return the built customizer
86+
*/
87+
StructuredLoggingJsonMembersCustomizer<T> build();
88+
89+
}
90+
5891
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizer.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizer implements Structured
3636

3737
private final StructuredLoggingJsonProperties properties;
3838

39+
private final boolean nested;
40+
3941
StructuredLoggingJsonPropertiesJsonMembersCustomizer(Instantiator<?> instantiator,
40-
StructuredLoggingJsonProperties properties) {
42+
StructuredLoggingJsonProperties properties, boolean nested) {
4143
this.instantiator = instantiator;
4244
this.properties = properties;
45+
this.nested = nested;
4346
}
4447

4548
@Override
@@ -48,7 +51,13 @@ public void customize(Members<Object> members) {
4851
members.applyingNameProcessor(this::renameJsonMembers);
4952
Map<String, String> add = this.properties.add();
5053
if (!CollectionUtils.isEmpty(add)) {
51-
add.forEach(members::add);
54+
if (this.nested) {
55+
ContextPairs contextPairs = new ContextPairs(true, "");
56+
members.add().usingPairs(contextPairs.nested((pairs) -> pairs.addMapEntries((source) -> add)));
57+
}
58+
else {
59+
add.forEach(members::add);
60+
}
5261
}
5362
this.properties.customizers(this.instantiator).forEach((customizer) -> customizer.customize(members));
5463
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/AbstractStructuredLoggingTests.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.mockito.Mock;
3232
import org.mockito.junit.jupiter.MockitoExtension;
3333

34+
import org.springframework.boot.logging.structured.MockStructuredLoggingJsonMembersCustomizerBuilder;
3435
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;
3536

3637
import static org.assertj.core.api.Assertions.assertThat;
@@ -50,6 +51,9 @@ abstract class AbstractStructuredLoggingTests {
5051
@Mock
5152
StructuredLoggingJsonMembersCustomizer<?> customizer;
5253

54+
MockStructuredLoggingJsonMembersCustomizerBuilder<?> customizerBuilder = new MockStructuredLoggingJsonMembersCustomizerBuilder<>(
55+
() -> this.customizer);
56+
5357
protected Map<String, Object> map(Object... values) {
5458
assertThat(values.length).isEven();
5559
Map<String, Object> result = new HashMap<>();

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ void setUp() {
5656
this.environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
5757
this.environment.setProperty("spring.application.pid", "1");
5858
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, null,
59-
TestContextPairs.include(), this.customizer);
59+
TestContextPairs.include(), this.customizerBuilder);
60+
}
61+
62+
@Test
63+
void callsNestedOnCustomizerBuilder() {
64+
assertThat(this.customizerBuilder.isNested()).isTrue();
6065
}
6166

6267
@Test
@@ -109,7 +114,7 @@ void shouldFormatException() {
109114
@SuppressWarnings("unchecked")
110115
void shouldFormatExceptionUsingStackTracePrinter() {
111116
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(),
112-
TestContextPairs.include(), this.customizer);
117+
TestContextPairs.include(), this.customizerBuilder);
113118
MutableLogEvent event = createEvent();
114119
event.setThrown(new RuntimeException("Boom"));
115120
Map<String, Object> deserialized = deserialize(this.formatter.format(event));

0 commit comments

Comments
 (0)