Skip to content

Commit de3b14f

Browse files
committed
Refine structured logging
Refine structured logging to support `Environment`, `ApplicationPid` and `ElasticCommonSchemaService` injection. With these updates we are able to remove the `ApplicationMetadata` class and simplify the parameters passed to the layout/encoder classes. Closes gh-41491
1 parent 50dbaec commit de3b14f

File tree

30 files changed

+539
-290
lines changed

30 files changed

+539
-290
lines changed

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/logging.adoc

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ Handling authenticated request
440440
== Structured Logging
441441

442442
Structured logging is a technique where the log output is written in a well-defined, often machine-readable format.
443-
Spring Boot supports structured logging and has support for the following formats out of the box:
443+
Spring Boot supports structured logging and has support for the following JSON formats out of the box:
444444

445445
* xref:#features.logging.structured.ecs[Elastic Common Schema (ECS)]
446446
* xref:#features.logging.structured.logstash[Logstash]
@@ -474,6 +474,22 @@ A log line looks like this:
474474
This format also adds every key value pair contained in the MDC to the JSON object.
475475
You can also use the https://www.slf4j.org/manual.html#fluent[SLF4J fluent logging API] to add key value pairs to the logged JSON object with the https://www.slf4j.org/apidocs/org/slf4j/spi/LoggingEventBuilder.html#addKeyValue(java.lang.String,java.lang.Object)[addKeyValue] method.
476476

477+
The `service` values can be customized using `logging.structured.ecs.service` properties:
478+
479+
[configprops,yaml]
480+
----
481+
logging:
482+
structured:
483+
ecs:
484+
service:
485+
name: MyService
486+
version: 1.0
487+
environment: Production
488+
node-name: Primary
489+
----
490+
491+
NOTE: configprop:logging.structured.ecs.service.name[] will default to configprop:spring.application.name[] if not specified.
492+
477493

478494

479495
[[features.logging.structured.logstash]]

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

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
import org.apache.logging.log4j.util.ReadOnlyStringMap;
2525

2626
import org.springframework.boot.json.JsonWriter;
27-
import org.springframework.boot.logging.structured.ApplicationMetadata;
2827
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
28+
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
29+
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
2930
import org.springframework.boot.logging.structured.StructuredLogFormatter;
31+
import org.springframework.boot.system.ApplicationPid;
3032
import org.springframework.util.ObjectUtils;
3133

3234
/**
@@ -36,23 +38,19 @@
3638
* @author Moritz Halbritter
3739
* @author Phillip Webb
3840
*/
39-
class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
41+
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
4042

41-
private final JsonWriter<LogEvent> writer;
42-
43-
ElasticCommonSchemaStructuredLogFormatter(ApplicationMetadata metadata) {
44-
this.writer = JsonWriter.<LogEvent>of((members) -> logEventJson(metadata, members)).withNewLineAtEnd();
43+
ElasticCommonSchemaStructuredLogFormatter(ApplicationPid pid, ElasticCommonSchemaService service) {
44+
super((members) -> jsonMembers(pid, service, members));
4545
}
4646

47-
private void logEventJson(ApplicationMetadata metadata, JsonWriter.Members<LogEvent> members) {
48-
members.add("@timestamp", LogEvent::getInstant).as(this::asTimestamp);
47+
private static void jsonMembers(ApplicationPid pid, ElasticCommonSchemaService service,
48+
JsonWriter.Members<LogEvent> members) {
49+
members.add("@timestamp", LogEvent::getInstant).as(ElasticCommonSchemaStructuredLogFormatter::asTimestamp);
4950
members.add("log.level", LogEvent::getLevel).as(Level::name);
50-
members.add("process.pid", metadata::pid).whenNotNull();
51+
members.add("process.pid", pid).when(ApplicationPid::isAvailable).as(ApplicationPid::toLong);
5152
members.add("process.thread.name", LogEvent::getThreadName);
52-
members.add("service.name", metadata::name).whenHasLength();
53-
members.add("service.version", metadata::version).whenHasLength();
54-
members.add("service.environment", metadata::environment).whenHasLength();
55-
members.add("service.node.name", metadata::nodeName).whenHasLength();
53+
service.jsonMembers(members);
5654
members.add("log.logger", LogEvent::getLoggerName);
5755
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
5856
members.add(LogEvent::getContextData)
@@ -68,13 +66,8 @@ private void logEventJson(ApplicationMetadata metadata, JsonWriter.Members<LogEv
6866
members.add("ecs.version", "8.11");
6967
}
7068

71-
private java.time.Instant asTimestamp(Instant instant) {
69+
private static java.time.Instant asTimestamp(Instant instant) {
7270
return java.time.Instant.ofEpochMilli(instant.getEpochMillisecond()).plusNanos(instant.getNanoOfMillisecond());
7371
}
7472

75-
@Override
76-
public String format(LogEvent event) {
77-
return this.writer.writeToString(event);
78-
}
79-
8073
}

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

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import org.springframework.boot.json.JsonWriter;
3434
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
35+
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
3536
import org.springframework.boot.logging.structured.StructuredLogFormatter;
3637
import org.springframework.util.CollectionUtils;
3738

@@ -41,16 +42,14 @@
4142
* @author Moritz Halbritter
4243
* @author Phillip Webb
4344
*/
44-
class LogstashStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
45-
46-
private JsonWriter<LogEvent> writer;
45+
class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
4746

4847
LogstashStructuredLogFormatter() {
49-
this.writer = JsonWriter.<LogEvent>of(this::logEventJson).withNewLineAtEnd();
48+
super(LogstashStructuredLogFormatter::jsonMembers);
5049
}
5150

52-
private void logEventJson(JsonWriter.Members<LogEvent> members) {
53-
members.add("@timestamp", LogEvent::getInstant).as(this::asTimestamp);
51+
private static void jsonMembers(JsonWriter.Members<LogEvent> members) {
52+
members.add("@timestamp", LogEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
5453
members.add("@version", "1");
5554
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
5655
members.add("logger_name", LogEvent::getLoggerName);
@@ -60,26 +59,29 @@ private void logEventJson(JsonWriter.Members<LogEvent> members) {
6059
members.add(LogEvent::getContextData)
6160
.whenNot(ReadOnlyStringMap::isEmpty)
6261
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));
63-
members.add("tags", LogEvent::getMarker).whenNotNull().as(this::getMarkers).whenNot(CollectionUtils::isEmpty);
62+
members.add("tags", LogEvent::getMarker)
63+
.whenNotNull()
64+
.as(LogstashStructuredLogFormatter::getMarkers)
65+
.whenNot(CollectionUtils::isEmpty);
6466
members.add("stack_trace", LogEvent::getThrownProxy)
6567
.whenNotNull()
6668
.as(ThrowableProxy::getExtendedStackTraceAsString);
6769
}
6870

69-
private String asTimestamp(Instant instant) {
71+
private static String asTimestamp(Instant instant) {
7072
java.time.Instant javaInstant = java.time.Instant.ofEpochMilli(instant.getEpochMillisecond())
7173
.plusNanos(instant.getNanoOfMillisecond());
7274
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(javaInstant, ZoneId.systemDefault());
7375
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime);
7476
}
7577

76-
private Set<String> getMarkers(Marker marker) {
78+
private static Set<String> getMarkers(Marker marker) {
7779
Set<String> result = new TreeSet<>();
7880
addMarkers(result, marker);
7981
return result;
8082
}
8183

82-
private void addMarkers(Set<String> result, Marker marker) {
84+
private static void addMarkers(Set<String> result, Marker marker) {
8385
result.add(marker.getName());
8486
if (marker.hasParents()) {
8587
for (Marker parent : marker.getParents()) {
@@ -88,9 +90,4 @@ private void addMarkers(Set<String> result, Marker marker) {
8890
}
8991
}
9092

91-
@Override
92-
public String format(LogEvent event) {
93-
return this.writer.writeToString(event);
94-
}
95-
9693
}

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

Lines changed: 18 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121

2222
import org.apache.logging.log4j.core.Layout;
2323
import org.apache.logging.log4j.core.LogEvent;
24+
import org.apache.logging.log4j.core.LoggerContext;
2425
import org.apache.logging.log4j.core.config.Node;
2526
import org.apache.logging.log4j.core.config.plugins.Plugin;
2627
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
2728
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
29+
import org.apache.logging.log4j.core.config.plugins.PluginLoggerContext;
2830
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
2931

30-
import org.springframework.boot.logging.structured.ApplicationMetadata;
3132
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
33+
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
3234
import org.springframework.boot.logging.structured.StructuredLogFormatter;
3335
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
3436
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
37+
import org.springframework.boot.system.ApplicationPid;
38+
import org.springframework.core.env.Environment;
3539
import org.springframework.util.Assert;
3640

3741
/**
@@ -57,34 +61,27 @@ public String toSerializable(LogEvent event) {
5761
return this.formatter.format(event);
5862
}
5963

64+
@Override
65+
public byte[] toByteArray(LogEvent event) {
66+
return this.formatter.formatAsBytes(event, (getCharset() != null) ? getCharset() : StandardCharsets.UTF_8);
67+
}
68+
6069
@PluginBuilderFactory
6170
static StructuredLogLayout.Builder newBuilder() {
6271
return new StructuredLogLayout.Builder();
6372
}
6473

6574
static final class Builder implements org.apache.logging.log4j.core.util.Builder<StructuredLogLayout> {
6675

76+
@PluginLoggerContext
77+
private LoggerContext loggerContext;
78+
6779
@PluginBuilderAttribute
6880
private String format;
6981

7082
@PluginBuilderAttribute
7183
private String charset = StandardCharsets.UTF_8.name();
7284

73-
@PluginBuilderAttribute
74-
private Long pid;
75-
76-
@PluginBuilderAttribute
77-
private String serviceName;
78-
79-
@PluginBuilderAttribute
80-
private String serviceVersion;
81-
82-
@PluginBuilderAttribute
83-
private String serviceNodeName;
84-
85-
@PluginBuilderAttribute
86-
private String serviceEnvironment;
87-
8885
Builder setFormat(String format) {
8986
this.format = format;
9087
return this;
@@ -95,46 +92,22 @@ Builder setCharset(String charset) {
9592
return this;
9693
}
9794

98-
Builder setPid(Long pid) {
99-
this.pid = pid;
100-
return this;
101-
}
102-
103-
Builder setServiceName(String serviceName) {
104-
this.serviceName = serviceName;
105-
return this;
106-
}
107-
108-
Builder setServiceVersion(String serviceVersion) {
109-
this.serviceVersion = serviceVersion;
110-
return this;
111-
}
112-
113-
Builder setServiceNodeName(String serviceNodeName) {
114-
this.serviceNodeName = serviceNodeName;
115-
return this;
116-
}
117-
118-
Builder setServiceEnvironment(String serviceEnvironment) {
119-
this.serviceEnvironment = serviceEnvironment;
120-
return this;
121-
}
122-
12395
@Override
12496
public StructuredLogLayout build() {
125-
ApplicationMetadata applicationMetadata = new ApplicationMetadata(this.pid, this.serviceName,
126-
this.serviceVersion, this.serviceEnvironment, this.serviceNodeName);
12797
Charset charset = Charset.forName(this.charset);
98+
Environment environment = Log4J2LoggingSystem.getEnvironment(this.loggerContext);
99+
Assert.state(environment != null, "Unable to find Spring Environment in logger context");
128100
StructuredLogFormatter<LogEvent> formatter = new StructuredLogFormatterFactory<>(LogEvent.class,
129-
applicationMetadata, null, this::addCommonFormatters)
101+
environment, null, this::addCommonFormatters)
130102
.get(this.format);
131103
return new StructuredLogLayout(charset, formatter);
132104
}
133105

134106
private void addCommonFormatters(CommonFormatters<LogEvent> commonFormatters) {
135107
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
136108
(instantiator) -> new ElasticCommonSchemaStructuredLogFormatter(
137-
instantiator.getArg(ApplicationMetadata.class)));
109+
instantiator.getArg(ApplicationPid.class),
110+
instantiator.getArg(ElasticCommonSchemaService.class)));
138111
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH,
139112
(instantiator) -> new LogstashStructuredLogFormatter());
140113
}

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,6 @@ private Encoder<ILoggingEvent> createEncoder(LogbackConfigurator config, String
174174
private StructuredLogEncoder createStructuredLoggingEncoder(LogbackConfigurator config, String format) {
175175
StructuredLogEncoder encoder = new StructuredLogEncoder();
176176
encoder.setFormat(format);
177-
encoder.setPid(resolveLong(config, "${PID:--1}"));
178-
String applicationName = resolve(config, "${APPLICATION_NAME:-}");
179-
if (StringUtils.hasLength(applicationName)) {
180-
encoder.setServiceName(applicationName);
181-
}
182177
return encoder;
183178
}
184179

@@ -205,10 +200,6 @@ private int resolveInt(LogbackConfigurator config, String val) {
205200
return Integer.parseInt(resolve(config, val));
206201
}
207202

208-
private long resolveLong(LogbackConfigurator config, String val) {
209-
return Long.parseLong(resolve(config, val));
210-
}
211-
212203
private FileSize resolveFileSize(LogbackConfigurator config, String val) {
213204
return FileSize.valueOf(resolve(config, val));
214205
}

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

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323

2424
import org.springframework.boot.json.JsonWriter;
2525
import org.springframework.boot.json.JsonWriter.PairExtractor;
26-
import org.springframework.boot.logging.structured.ApplicationMetadata;
2726
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
27+
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
28+
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
2829
import org.springframework.boot.logging.structured.StructuredLogFormatter;
30+
import org.springframework.boot.system.ApplicationPid;
2931

3032
/**
3133
* Logback {@link StructuredLogFormatter} for
@@ -34,30 +36,23 @@
3436
* @author Moritz Halbritter
3537
* @author Phillip Webb
3638
*/
37-
class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatter<ILoggingEvent> {
39+
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {
3840

3941
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
4042
(pair) -> pair.value);
4143

42-
private JsonWriter<ILoggingEvent> writer;
43-
44-
ElasticCommonSchemaStructuredLogFormatter(ApplicationMetadata metadata,
44+
ElasticCommonSchemaStructuredLogFormatter(ApplicationPid pid, ElasticCommonSchemaService service,
4545
ThrowableProxyConverter throwableProxyConverter) {
46-
this.writer = JsonWriter
47-
.<ILoggingEvent>of((members) -> loggingEventJson(metadata, throwableProxyConverter, members))
48-
.withNewLineAtEnd();
46+
super((members) -> jsonMembers(pid, service, throwableProxyConverter, members));
4947
}
5048

51-
private void loggingEventJson(ApplicationMetadata metadata, ThrowableProxyConverter throwableProxyConverter,
52-
JsonWriter.Members<ILoggingEvent> members) {
49+
private static void jsonMembers(ApplicationPid pid, ElasticCommonSchemaService service,
50+
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
5351
members.add("@timestamp", ILoggingEvent::getInstant);
5452
members.add("log.level", ILoggingEvent::getLevel);
55-
members.add("process.pid", metadata::pid).whenNotNull();
53+
members.add("process.pid", pid).when(ApplicationPid::isAvailable).as(ApplicationPid::toLong);
5654
members.add("process.thread.name", ILoggingEvent::getThreadName);
57-
members.add("service.name", metadata::name).whenHasLength();
58-
members.add("service.version", metadata::version).whenHasLength();
59-
members.add("service.environment", metadata::environment).whenHasLength();
60-
members.add("service.node.name", metadata::nodeName).whenHasLength();
55+
service.jsonMembers(members);
6156
members.add("log.logger", ILoggingEvent::getLoggerName);
6257
members.add("message", ILoggingEvent::getFormattedMessage);
6358
members.addMapEntries(ILoggingEvent::getMDCPropertyMap);
@@ -72,9 +67,4 @@ private void loggingEventJson(ApplicationMetadata metadata, ThrowableProxyConver
7267
members.add("ecs.version", "8.11");
7368
}
7469

75-
@Override
76-
public String format(ILoggingEvent event) {
77-
return this.writer.writeToString(event);
78-
}
79-
8070
}

0 commit comments

Comments
 (0)