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
This commit is contained in:
Phillip Webb 2024-07-24 17:59:45 +01:00
parent 50dbaec2d0
commit de3b14f2b4
30 changed files with 539 additions and 290 deletions

View File

@ -440,7 +440,7 @@ Handling authenticated request
== Structured Logging
Structured logging is a technique where the log output is written in a well-defined, often machine-readable format.
Spring Boot supports structured logging and has support for the following formats out of the box:
Spring Boot supports structured logging and has support for the following JSON formats out of the box:
* xref:#features.logging.structured.ecs[Elastic Common Schema (ECS)]
* xref:#features.logging.structured.logstash[Logstash]
@ -474,6 +474,22 @@ A log line looks like this:
This format also adds every key value pair contained in the MDC to the JSON object.
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.
The `service` values can be customized using `logging.structured.ecs.service` properties:
[configprops,yaml]
----
logging:
structured:
ecs:
service:
name: MyService
version: 1.0
environment: Production
node-name: Primary
----
NOTE: configprop:logging.structured.ecs.service.name[] will default to configprop:spring.application.name[] if not specified.
[[features.logging.structured.logstash]]

View File

@ -24,9 +24,11 @@ import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.util.ObjectUtils;
/**
@ -36,23 +38,19 @@ import org.springframework.util.ObjectUtils;
* @author Moritz Halbritter
* @author Phillip Webb
*/
class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
private final JsonWriter<LogEvent> writer;
ElasticCommonSchemaStructuredLogFormatter(ApplicationMetadata metadata) {
this.writer = JsonWriter.<LogEvent>of((members) -> logEventJson(metadata, members)).withNewLineAtEnd();
ElasticCommonSchemaStructuredLogFormatter(ApplicationPid pid, ElasticCommonSchemaService service) {
super((members) -> jsonMembers(pid, service, members));
}
private void logEventJson(ApplicationMetadata metadata, JsonWriter.Members<LogEvent> members) {
members.add("@timestamp", LogEvent::getInstant).as(this::asTimestamp);
private static void jsonMembers(ApplicationPid pid, ElasticCommonSchemaService service,
JsonWriter.Members<LogEvent> members) {
members.add("@timestamp", LogEvent::getInstant).as(ElasticCommonSchemaStructuredLogFormatter::asTimestamp);
members.add("log.level", LogEvent::getLevel).as(Level::name);
members.add("process.pid", metadata::pid).whenNotNull();
members.add("process.pid", pid).when(ApplicationPid::isAvailable).as(ApplicationPid::toLong);
members.add("process.thread.name", LogEvent::getThreadName);
members.add("service.name", metadata::name).whenHasLength();
members.add("service.version", metadata::version).whenHasLength();
members.add("service.environment", metadata::environment).whenHasLength();
members.add("service.node.name", metadata::nodeName).whenHasLength();
service.jsonMembers(members);
members.add("log.logger", LogEvent::getLoggerName);
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
members.add(LogEvent::getContextData)
@ -68,13 +66,8 @@ class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatte
members.add("ecs.version", "8.11");
}
private java.time.Instant asTimestamp(Instant instant) {
private static java.time.Instant asTimestamp(Instant instant) {
return java.time.Instant.ofEpochMilli(instant.getEpochMillisecond()).plusNanos(instant.getNanoOfMillisecond());
}
@Override
public String format(LogEvent event) {
return this.writer.writeToString(event);
}
}

View File

@ -32,6 +32,7 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.util.CollectionUtils;
@ -41,16 +42,14 @@ import org.springframework.util.CollectionUtils;
* @author Moritz Halbritter
* @author Phillip Webb
*/
class LogstashStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
private JsonWriter<LogEvent> writer;
class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
LogstashStructuredLogFormatter() {
this.writer = JsonWriter.<LogEvent>of(this::logEventJson).withNewLineAtEnd();
super(LogstashStructuredLogFormatter::jsonMembers);
}
private void logEventJson(JsonWriter.Members<LogEvent> members) {
members.add("@timestamp", LogEvent::getInstant).as(this::asTimestamp);
private static void jsonMembers(JsonWriter.Members<LogEvent> members) {
members.add("@timestamp", LogEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
members.add("@version", "1");
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
members.add("logger_name", LogEvent::getLoggerName);
@ -60,26 +59,29 @@ class LogstashStructuredLogFormatter implements StructuredLogFormatter<LogEvent>
members.add(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));
members.add("tags", LogEvent::getMarker).whenNotNull().as(this::getMarkers).whenNot(CollectionUtils::isEmpty);
members.add("tags", LogEvent::getMarker)
.whenNotNull()
.as(LogstashStructuredLogFormatter::getMarkers)
.whenNot(CollectionUtils::isEmpty);
members.add("stack_trace", LogEvent::getThrownProxy)
.whenNotNull()
.as(ThrowableProxy::getExtendedStackTraceAsString);
}
private String asTimestamp(Instant instant) {
private static String asTimestamp(Instant instant) {
java.time.Instant javaInstant = java.time.Instant.ofEpochMilli(instant.getEpochMillisecond())
.plusNanos(instant.getNanoOfMillisecond());
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(javaInstant, ZoneId.systemDefault());
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime);
}
private Set<String> getMarkers(Marker marker) {
private static Set<String> getMarkers(Marker marker) {
Set<String> result = new TreeSet<>();
addMarkers(result, marker);
return result;
}
private void addMarkers(Set<String> result, Marker marker) {
private static void addMarkers(Set<String> result, Marker marker) {
result.add(marker.getName());
if (marker.hasParents()) {
for (Marker parent : marker.getParents()) {
@ -88,9 +90,4 @@ class LogstashStructuredLogFormatter implements StructuredLogFormatter<LogEvent>
}
}
@Override
public String format(LogEvent event) {
return this.writer.writeToString(event);
}
}

View File

@ -21,17 +21,21 @@ import java.nio.charset.StandardCharsets;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Node;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import org.apache.logging.log4j.core.config.plugins.PluginLoggerContext;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
/**
@ -57,6 +61,11 @@ final class StructuredLogLayout extends AbstractStringLayout {
return this.formatter.format(event);
}
@Override
public byte[] toByteArray(LogEvent event) {
return this.formatter.formatAsBytes(event, (getCharset() != null) ? getCharset() : StandardCharsets.UTF_8);
}
@PluginBuilderFactory
static StructuredLogLayout.Builder newBuilder() {
return new StructuredLogLayout.Builder();
@ -64,27 +73,15 @@ final class StructuredLogLayout extends AbstractStringLayout {
static final class Builder implements org.apache.logging.log4j.core.util.Builder<StructuredLogLayout> {
@PluginLoggerContext
private LoggerContext loggerContext;
@PluginBuilderAttribute
private String format;
@PluginBuilderAttribute
private String charset = StandardCharsets.UTF_8.name();
@PluginBuilderAttribute
private Long pid;
@PluginBuilderAttribute
private String serviceName;
@PluginBuilderAttribute
private String serviceVersion;
@PluginBuilderAttribute
private String serviceNodeName;
@PluginBuilderAttribute
private String serviceEnvironment;
Builder setFormat(String format) {
this.format = format;
return this;
@ -95,38 +92,13 @@ final class StructuredLogLayout extends AbstractStringLayout {
return this;
}
Builder setPid(Long pid) {
this.pid = pid;
return this;
}
Builder setServiceName(String serviceName) {
this.serviceName = serviceName;
return this;
}
Builder setServiceVersion(String serviceVersion) {
this.serviceVersion = serviceVersion;
return this;
}
Builder setServiceNodeName(String serviceNodeName) {
this.serviceNodeName = serviceNodeName;
return this;
}
Builder setServiceEnvironment(String serviceEnvironment) {
this.serviceEnvironment = serviceEnvironment;
return this;
}
@Override
public StructuredLogLayout build() {
ApplicationMetadata applicationMetadata = new ApplicationMetadata(this.pid, this.serviceName,
this.serviceVersion, this.serviceEnvironment, this.serviceNodeName);
Charset charset = Charset.forName(this.charset);
Environment environment = Log4J2LoggingSystem.getEnvironment(this.loggerContext);
Assert.state(environment != null, "Unable to find Spring Environment in logger context");
StructuredLogFormatter<LogEvent> formatter = new StructuredLogFormatterFactory<>(LogEvent.class,
applicationMetadata, null, this::addCommonFormatters)
environment, null, this::addCommonFormatters)
.get(this.format);
return new StructuredLogLayout(charset, formatter);
}
@ -134,7 +106,8 @@ final class StructuredLogLayout extends AbstractStringLayout {
private void addCommonFormatters(CommonFormatters<LogEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new ElasticCommonSchemaStructuredLogFormatter(
instantiator.getArg(ApplicationMetadata.class)));
instantiator.getArg(ApplicationPid.class),
instantiator.getArg(ElasticCommonSchemaService.class)));
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH,
(instantiator) -> new LogstashStructuredLogFormatter());
}

View File

@ -174,11 +174,6 @@ class DefaultLogbackConfiguration {
private StructuredLogEncoder createStructuredLoggingEncoder(LogbackConfigurator config, String format) {
StructuredLogEncoder encoder = new StructuredLogEncoder();
encoder.setFormat(format);
encoder.setPid(resolveLong(config, "${PID:--1}"));
String applicationName = resolve(config, "${APPLICATION_NAME:-}");
if (StringUtils.hasLength(applicationName)) {
encoder.setServiceName(applicationName);
}
return encoder;
}
@ -205,10 +200,6 @@ class DefaultLogbackConfiguration {
return Integer.parseInt(resolve(config, val));
}
private long resolveLong(LogbackConfigurator config, String val) {
return Long.parseLong(resolve(config, val));
}
private FileSize resolveFileSize(LogbackConfigurator config, String val) {
return FileSize.valueOf(resolve(config, val));
}

View File

@ -23,9 +23,11 @@ import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.system.ApplicationPid;
/**
* Logback {@link StructuredLogFormatter} for
@ -34,30 +36,23 @@ import org.springframework.boot.logging.structured.StructuredLogFormatter;
* @author Moritz Halbritter
* @author Phillip Webb
*/
class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatter<ILoggingEvent> {
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
private JsonWriter<ILoggingEvent> writer;
ElasticCommonSchemaStructuredLogFormatter(ApplicationMetadata metadata,
ElasticCommonSchemaStructuredLogFormatter(ApplicationPid pid, ElasticCommonSchemaService service,
ThrowableProxyConverter throwableProxyConverter) {
this.writer = JsonWriter
.<ILoggingEvent>of((members) -> loggingEventJson(metadata, throwableProxyConverter, members))
.withNewLineAtEnd();
super((members) -> jsonMembers(pid, service, throwableProxyConverter, members));
}
private void loggingEventJson(ApplicationMetadata metadata, ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
private static void jsonMembers(ApplicationPid pid, ElasticCommonSchemaService service,
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
members.add("@timestamp", ILoggingEvent::getInstant);
members.add("log.level", ILoggingEvent::getLevel);
members.add("process.pid", metadata::pid).whenNotNull();
members.add("process.pid", pid).when(ApplicationPid::isAvailable).as(ApplicationPid::toLong);
members.add("process.thread.name", ILoggingEvent::getThreadName);
members.add("service.name", metadata::name).whenHasLength();
members.add("service.version", metadata::version).whenHasLength();
members.add("service.environment", metadata::environment).whenHasLength();
members.add("service.node.name", metadata::nodeName).whenHasLength();
service.jsonMembers(members);
members.add("log.logger", ILoggingEvent::getLoggerName);
members.add("message", ILoggingEvent::getFormattedMessage);
members.addMapEntries(ILoggingEvent::getMDCPropertyMap);
@ -72,9 +67,4 @@ class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatte
members.add("ecs.version", "8.11");
}
@Override
public String format(ILoggingEvent event) {
return this.writer.writeToString(event);
}
}

View File

@ -186,13 +186,13 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
putInitializationContextObjects(loggerContext, initializationContext);
if (isAlreadyInitialized(loggerContext)) {
return;
}
if (!initializeFromAotGeneratedArtifactsIfPossible(initializationContext, logFile)) {
super.initialize(initializationContext, configLocation, logFile);
}
loggerContext.putObject(Environment.class.getName(), initializationContext.getEnvironment());
loggerContext.getTurboFilterList().remove(SUPPRESS_ALL_FILTER);
markAsInitialized(loggerContext);
if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {
@ -211,6 +211,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
}
LoggerContext loggerContext = getLoggerContext();
stopAndReset(loggerContext);
withLoggingSuppressed(() -> putInitializationContextObjects(loggerContext, initializationContext));
SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);
configurator.setContext(loggerContext);
boolean configuredUsingAotGeneratedArtifacts = configurator.configureUsingAotGeneratedArtifacts();
@ -222,21 +223,23 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
LoggerContext context = getLoggerContext();
stopAndReset(context);
LoggerContext loggerContext = getLoggerContext();
stopAndReset(loggerContext);
withLoggingSuppressed(() -> {
putInitializationContextObjects(loggerContext, initializationContext);
boolean debug = Boolean.getBoolean("logback.debug");
if (debug) {
StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
StatusListenerConfigHelper.addOnConsoleListenerInstance(loggerContext, new OnConsoleStatusListener());
}
Environment environment = initializationContext.getEnvironment();
// Apply system properties directly in case the same JVM runs multiple apps
new LogbackLoggingSystemProperties(environment, getDefaultValueResolver(environment), context::putProperty)
new LogbackLoggingSystemProperties(environment, getDefaultValueResolver(environment),
loggerContext::putProperty)
.apply(logFile);
LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
: new LogbackConfigurator(context);
LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(loggerContext)
: new LogbackConfigurator(loggerContext);
new DefaultLogbackConfiguration(logFile).apply(configurator);
context.setPackagingDataEnabled(true);
loggerContext.setPackagingDataEnabled(true);
});
}
@ -246,6 +249,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
LoggerContext loggerContext = getLoggerContext();
stopAndReset(loggerContext);
withLoggingSuppressed(() -> {
putInitializationContextObjects(loggerContext, initializationContext);
if (initializationContext != null) {
applySystemProperties(initializationContext.getEnvironment(), logFile);
}
@ -334,11 +338,18 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
@Override
protected void reinitialize(LoggingInitializationContext initializationContext) {
getLoggerContext().reset();
getLoggerContext().getStatusManager().clear();
LoggerContext loggerContext = getLoggerContext();
loggerContext.reset();
loggerContext.getStatusManager().clear();
loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
}
private void putInitializationContextObjects(LoggerContext loggerContext,
LoggingInitializationContext initializationContext) {
withLoggingSuppressed(
() -> loggerContext.putObject(Environment.class.getName(), initializationContext.getEnvironment()));
}
@Override
public List<LoggerConfiguration> getLoggerConfigurations() {
List<LoggerConfiguration> result = new ArrayList<>();

View File

@ -34,6 +34,7 @@ import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
/**
@ -42,21 +43,18 @@ import org.springframework.boot.logging.structured.StructuredLogFormatter;
* @author Moritz Halbritter
* @author Phillip Webb
*/
class LogstashStructuredLogFormatter implements StructuredLogFormatter<ILoggingEvent> {
class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
private JsonWriter<ILoggingEvent> writer;
LogstashStructuredLogFormatter(ThrowableProxyConverter throwableProxyConverter) {
this.writer = JsonWriter.<ILoggingEvent>of((members) -> loggingEventJson(throwableProxyConverter, members))
.withNewLineAtEnd();
super((members) -> jsonMembers(throwableProxyConverter, members));
}
private void loggingEventJson(ThrowableProxyConverter throwableProxyConverter,
private static void jsonMembers(ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
members.add("@timestamp", ILoggingEvent::getInstant).as(this::asTimestamp);
members.add("@timestamp", ILoggingEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
members.add("@version", "1");
members.add("message", ILoggingEvent::getFormattedMessage);
members.add("logger_name", ILoggingEvent::getLoggerName);
@ -67,24 +65,27 @@ class LogstashStructuredLogFormatter implements StructuredLogFormatter<ILoggingE
members.add(ILoggingEvent::getKeyValuePairs)
.whenNotEmpty()
.usingExtractedPairs(Iterable::forEach, keyValuePairExtractor);
members.add("tags", ILoggingEvent::getMarkerList).whenNotNull().as(this::getMarkers).whenNotEmpty();
members.add("tags", ILoggingEvent::getMarkerList)
.whenNotNull()
.as(LogstashStructuredLogFormatter::getMarkers)
.whenNotEmpty();
members.add("stack_trace", (event) -> event)
.whenNotNull(ILoggingEvent::getThrowableProxy)
.as(throwableProxyConverter::convert);
}
private String asTimestamp(Instant instant) {
private static String asTimestamp(Instant instant) {
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(instant, ZoneId.systemDefault());
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime);
}
private Set<String> getMarkers(List<Marker> markers) {
private static Set<String> getMarkers(List<Marker> markers) {
Set<String> result = new LinkedHashSet<>();
addMarkers(result, markers.iterator());
return result;
}
private void addMarkers(Set<String> result, Iterator<Marker> iterator) {
private static void addMarkers(Set<String> result, Iterator<Marker> iterator) {
while (iterator.hasNext()) {
Marker marker = iterator.next();
result.add(marker.getName());
@ -94,9 +95,4 @@ class LogstashStructuredLogFormatter implements StructuredLogFormatter<ILoggingE
}
}
@Override
public String format(ILoggingEvent event) {
return this.writer.writeToString(event);
}
}

View File

@ -24,12 +24,14 @@ import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.encoder.EncoderBase;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
/**
@ -48,42 +50,12 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
private StructuredLogFormatter<ILoggingEvent> formatter;
private Long pid;
private String serviceName;
private String serviceVersion;
private String serviceNodeName;
private String serviceEnvironment;
private Charset charset = StandardCharsets.UTF_8;
public void setFormat(String format) {
this.format = format;
}
public void setPid(Long pid) {
this.pid = pid;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
public void setServiceVersion(String serviceVersion) {
this.serviceVersion = serviceVersion;
}
public void setServiceNodeName(String serviceNodeName) {
this.serviceNodeName = serviceNodeName;
}
public void setServiceEnvironment(String serviceEnvironment) {
this.serviceEnvironment = serviceEnvironment;
}
public void setCharset(Charset charset) {
this.charset = charset;
}
@ -97,10 +69,10 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
}
private StructuredLogFormatter<ILoggingEvent> createFormatter(String format) {
ApplicationMetadata applicationMetadata = new ApplicationMetadata(this.pid, this.serviceName,
this.serviceVersion, this.serviceEnvironment, this.serviceNodeName);
return new StructuredLogFormatterFactory<>(ILoggingEvent.class, applicationMetadata,
this::addAvailableParameters, this::addCommonFormatters)
Environment environment = (Environment) getContext().getObject(Environment.class.getName());
Assert.state(environment != null, "Unable to find Spring Environment in logger context");
return new StructuredLogFormatterFactory<>(ILoggingEvent.class, environment, this::addAvailableParameters,
this::addCommonFormatters)
.get(format);
}
@ -111,7 +83,8 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
private void addCommonFormatters(CommonFormatters<ILoggingEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new ElasticCommonSchemaStructuredLogFormatter(
instantiator.getArg(ApplicationMetadata.class),
instantiator.getArg(ApplicationPid.class),
instantiator.getArg(ElasticCommonSchemaService.class),
instantiator.getArg(ThrowableProxyConverter.class)));
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH, (instantiator) -> new LogstashStructuredLogFormatter(
instantiator.getArg(ThrowableProxyConverter.class)));
@ -130,7 +103,7 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
@Override
public byte[] encode(ILoggingEvent event) {
return this.formatter.format(event).getBytes(this.charset);
return this.formatter.formatAsBytes(event, (this.charset != null) ? this.charset : StandardCharsets.UTF_8);
}
@Override

View File

@ -1,32 +0,0 @@
/*
* Copyright 2012-2024 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;
/**
* Metadata about the application.
*
* @param pid the process ID of the application
* @param name the application name
* @param version the version of the application
* @param environment the name of the environment the application is running in
* @param nodeName the name of the node the application is running on
* @author Moritz Halbritter
* @since 3.4.0
*/
public record ApplicationMetadata(Long pid, String name, String version, String environment, String nodeName) {
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2012-2024 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 org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.json.JsonWriter;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
/**
* Service details for Elastic Common Schema structured logging.
*
* @param name the application name
* @param version the version of the application
* @param environment the name of the environment the application is running in
* @param nodeName the name of the node the application is running on
* @author Moritz Halbritter
* @author Phillip Webb
* @since 3.4.0
*/
public record ElasticCommonSchemaService(String name, String version, String environment, String nodeName) {
static final ElasticCommonSchemaService NONE = new ElasticCommonSchemaService(null, null, null, null);
private ElasticCommonSchemaService withDefaults(Environment environment) {
String name = withFallbackProperty(environment, this.name, "spring.application.name");
return new ElasticCommonSchemaService(name, this.version, this.environment, this.nodeName);
}
private String withFallbackProperty(Environment environment, String value, String property) {
return (!StringUtils.hasLength(value)) ? environment.getProperty(property) : value;
}
/**
* Add {@link JsonWriter} members for the service.
* @param members the members to add to
*/
public void jsonMembers(JsonWriter.Members<?> members) {
members.add("service.name", this::name).whenHasLength();
members.add("service.version", this::version).whenHasLength();
members.add("service.environment", this::environment).whenHasLength();
members.add("service.node.name", this::nodeName).whenHasLength();
}
/**
* Return a new {@link ElasticCommonSchemaService} from bound from properties in the
* given {@link Environment}.
* @param environment the source environment
* @return a new {@link ElasticCommonSchemaService} instance
*/
public static ElasticCommonSchemaService get(Environment environment) {
return Binder.get(environment)
.bind("logging.structured.ecs.service", ElasticCommonSchemaService.class)
.orElse(NONE)
.withDefaults(environment);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2012-2024 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.nio.charset.Charset;
import java.util.function.Consumer;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
/**
* Base class for {@link StructuredLogFormatter} implementations that generates JSON using
* a {@link JsonWriter}.
*
* @param <E> the log event type
* @author Phillip Webb
* @since 3.4.0
*/
public abstract class JsonWriterStructuredLogFormatter<E> implements StructuredLogFormatter<E> {
private final JsonWriter<E> jsonWriter;
/**
* Create a new {@link JsonWriterStructuredLogFormatter} instance with the given
* members.
* @param members a consumer which should configure the members
*/
protected JsonWriterStructuredLogFormatter(Consumer<Members<E>> members) {
this(JsonWriter.of(members).withNewLineAtEnd());
}
/**
* Create a new {@link JsonWriterStructuredLogFormatter} instance with the given
* {@link JsonWriter}.
* @param jsonWriter the {@link JsonWriter}
*/
protected JsonWriterStructuredLogFormatter(JsonWriter<E> jsonWriter) {
this.jsonWriter = jsonWriter;
}
@Override
public String format(E event) {
return this.jsonWriter.writeToString(event);
}
@Override
public byte[] formatAsBytes(E event, Charset charset) {
return this.jsonWriter.write(event).toByteArray();
}
}

View File

@ -16,14 +16,21 @@
package org.springframework.boot.logging.structured;
import java.nio.charset.Charset;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.core.env.Environment;
/**
* Formats a log event to a structured log message.
* <p>
* Implementing classes can declare the following parameter types in the constructor:
* <ul>
* <li>{@link ApplicationMetadata}</li>
* <li>{@link Environment}</li>
* <li>{@link ApplicationPid}</li>
* <li>{@link ElasticCommonSchemaService}</li>
* </ul>
* When using Logback, implementing classes can also use the following parameter types in
* the constructor:
@ -35,13 +42,24 @@ import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
* @author Moritz Halbritter
* @since 3.4.0
*/
@FunctionalInterface
public interface StructuredLogFormatter<E> {
/**
* Formats the given log event.
* Formats the given log event to a String.
* @param event the log event to write
* @return the formatted log event
* @return the formatted log event String
*/
String format(E event);
/**
* Formats the given log event to a byte array.
* @param event the log event to write
* @param charset the charset
* @return the formatted log event bytes
*/
default byte[] formatAsBytes(E event, Charset charset) {
return format(event).getBytes(charset);
}
}

View File

@ -21,10 +21,12 @@ import java.util.Map;
import java.util.TreeMap;
import java.util.function.Consumer;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.boot.util.Instantiator;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.boot.util.Instantiator.FailureHandler;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
/**
@ -56,16 +58,19 @@ public class StructuredLogFormatterFactory<E> {
/**
* Create a new {@link StructuredLogFormatterFactory} instance.
* @param logEventType the log event type
* @param applicationMetadata an {@link ApplicationMetadata} instance for injection
* @param environment the Spring {@link Environment}
* @param availableParameters callback used to configure available parameters for the
* specific logging system
* @param commonFormatters callback used to define supported common formatters
*/
public StructuredLogFormatterFactory(Class<E> logEventType, ApplicationMetadata applicationMetadata,
public StructuredLogFormatterFactory(Class<E> logEventType, Environment environment,
Consumer<AvailableParameters> availableParameters, Consumer<CommonFormatters<E>> commonFormatters) {
this.logEventType = logEventType;
this.instantiator = new Instantiator<>(StructuredLogFormatter.class, (allAvailableParameters) -> {
allAvailableParameters.add(ApplicationMetadata.class, applicationMetadata);
allAvailableParameters.add(Environment.class, environment);
allAvailableParameters.add(ApplicationPid.class, (type) -> new ApplicationPid());
allAvailableParameters.add(ElasticCommonSchemaService.class,
(type) -> ElasticCommonSchemaService.get(environment));
if (availableParameters != null) {
availableParameters.accept(allAvailableParameters);
}

View File

@ -223,6 +223,26 @@
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
"defaultValue": true
},
{
"name": "logging.structured.ecs.service.environment",
"type": "java.lang.String",
"description": "Structured ECS service environment."
},
{
"name": "logging.structured.ecs.service.name",
"type": "java.lang.String",
"description": "Structured ECS service name (defaults to 'spring.application.name')."
},
{
"name": "logging.structured.ecs.service.node-name",
"type": "java.lang.String",
"description": "Structured ECS service node name."
},
{
"name": "logging.structured.ecs.service.version",
"type": "java.lang.String",
"description": "Structured ECS service version."
},
{
"name": "logging.structured.format.console",
"type": "java.lang.String",
@ -597,6 +617,44 @@
}
]
},
{
"name": "logging.structured.format.console",
"values": [
{
"value": "ecs"
},
{
"value": "logstash"
}
],
"providers": [
{
"name": "handle-as",
"parameters": {
"target": "java.lang.Class"
}
}
]
},
{
"name": "logging.structured.format.file",
"values": [
{
"value": "ecs"
},
{
"value": "logstash"
}
],
"providers": [
{
"name": "handle-as",
"parameters": {
"target": "java.lang.Class"
}
}
]
},
{
"name": "spring.config.import",
"values": [

View File

@ -11,7 +11,7 @@
<Console name="Console" target="SYSTEM_OUT" follow="true">
<Select>
<SystemPropertyArbiter propertyName="CONSOLE_LOG_STRUCTURED_FORMAT">
<StructuredLogLayout format="${sys:CONSOLE_LOG_STRUCTURED_FORMAT}" charset="${sys:CONSOLE_LOG_CHARSET}" pid="${sys:PID:--1}" serviceName="${sys:APPLICATION_NAME:-}"/>
<StructuredLogLayout format="${sys:CONSOLE_LOG_STRUCTURED_FORMAT}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
</SystemPropertyArbiter>
<DefaultArbiter>
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
@ -24,7 +24,7 @@
<RollingFile name="File" fileName="${sys:LOG_FILE}" filePattern="${sys:LOG_PATH}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">
<Select>
<SystemPropertyArbiter propertyName="FILE_LOG_STRUCTURED_FORMAT">
<StructuredLogLayout format="${sys:FILE_LOG_STRUCTURED_FORMAT}" charset="${sys:FILE_LOG_CHARSET}" pid="${sys:PID:--1}" serviceName="${sys:APPLICATION_NAME:-}"/>
<StructuredLogLayout format="${sys:FILE_LOG_STRUCTURED_FORMAT}" charset="${sys:FILE_LOG_CHARSET}"/>
</SystemPropertyArbiter>
<DefaultArbiter>
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}" charset="${sys:FILE_LOG_CHARSET}"/>

View File

@ -11,7 +11,7 @@
<Console name="Console" target="SYSTEM_OUT" follow="true">
<Select>
<SystemPropertyArbiter propertyName="CONSOLE_LOG_STRUCTURED_FORMAT">
<StructuredLogLayout format="${sys:CONSOLE_LOG_STRUCTURED_FORMAT}" charset="${sys:CONSOLE_LOG_CHARSET}" pid="${sys:PID:--1}" serviceName="${sys:APPLICATION_NAME:-}"/>
<StructuredLogLayout format="${sys:CONSOLE_LOG_STRUCTURED_FORMAT}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
</SystemPropertyArbiter>
<DefaultArbiter>
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>

View File

@ -13,8 +13,6 @@ equivalent to the programmatic initialization performed by Boot
<encoder class="org.springframework.boot.logging.logback.StructuredLoggingEncoder">
<format>${CONSOLE_LOG_STRUCTURED_FORMAT}</format>
<charset>${CONSOLE_LOG_CHARSET}</charset>
<pid>${PID:--1}</pid>
<serviceName>${APPLICATION_NAME:-}</serviceName>
</encoder>
</appender>
</included>

View File

@ -13,8 +13,6 @@ equivalent to the programmatic initialization performed by Boot
<encoder class="org.springframework.boot.logging.logback.StructuredLoggingEncoder">
<format>${FILE_LOG_STRUCTURED_FORMAT}</format>
<charset>${FILE_LOG_CHARSET}</charset>
<pid>${PID:--1}</pid>
<serviceName>${APPLICATION_NAME:-}</serviceName>
</encoder>
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">

View File

@ -23,7 +23,9 @@ import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.boot.system.MockApplicationPid;
import static org.assertj.core.api.Assertions.assertThat;
@ -32,14 +34,15 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Moritz Halbritter
*/
class Log4j2EcsStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private ElasticCommonSchemaStructuredLogFormatter formatter;
@BeforeEach
void setUp() {
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(
new ApplicationMetadata(1L, "name", "1.0.0", "test", "node-1"));
ApplicationPid pid = MockApplicationPid.of(1L);
ElasticCommonSchemaService service = new ElasticCommonSchemaService("name", "1.0.0", "test", "node-1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(pid, service);
}
@Test
@ -67,10 +70,10 @@ class Log4j2EcsStructuredLoggingFormatterTests extends AbstractStructuredLogging
assertThat(stackTrace).startsWith(
"""
java.lang.RuntimeException: Boom
\tat org.springframework.boot.logging.log4j2.Log4j2EcsStructuredLoggingFormatterTests.shouldFormatException""");
\tat org.springframework.boot.logging.log4j2.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException""");
assertThat(json).contains(
"""
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.Log4j2EcsStructuredLoggingFormatterTests.shouldFormatException""");
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException""");
}
}

View File

@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Moritz Halbritter
*/
class Log4j2LogstashStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private LogstashStructuredLogFormatter formatter;
@ -71,10 +71,10 @@ class Log4j2LogstashStructuredLoggingFormatterTests extends AbstractStructuredLo
assertThat(stackTrace).startsWith(
"""
java.lang.RuntimeException: Boom
\tat org.springframework.boot.logging.log4j2.Log4j2LogstashStructuredLoggingFormatterTests.shouldFormatException""");
\tat org.springframework.boot.logging.log4j2.LogstashStructuredLogFormatterTests.shouldFormatException""");
assertThat(json).contains(
"""
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.Log4j2LogstashStructuredLoggingFormatterTests.shouldFormatException""");
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.LogstashStructuredLogFormatterTests.shouldFormatException""");
}
}

View File

@ -18,11 +18,18 @@ package org.springframework.boot.logging.log4j2;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.log4j2.StructuredLogLayout.Builder;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -34,9 +41,25 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/
class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
private MockEnvironment environment;
private LoggerContext loggerContext;
@BeforeEach
void setup() {
this.environment = new MockEnvironment();
this.loggerContext = (LoggerContext) LogManager.getContext(false);
this.loggerContext.putObject(Log4J2LoggingSystem.ENVIRONMENT_KEY, this.environment);
}
@AfterEach
void cleanup() {
this.loggerContext.removeObject(Log4J2LoggingSystem.ENVIRONMENT_KEY);
}
@Test
void shouldSupportEcsCommonFormat() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder().setFormat("ecs").build();
StructuredLogLayout layout = newBuilder().setFormat("ecs").build();
String json = layout.toSerializable(createEvent());
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("ecs.version");
@ -44,7 +67,7 @@ class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
@Test
void shouldSupportLogstashCommonFormat() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder().setFormat("logstash").build();
StructuredLogLayout layout = newBuilder().setFormat("logstash").build();
String json = layout.toSerializable(createEvent());
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("@version");
@ -52,8 +75,7 @@ class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
@Test
void shouldSupportCustomFormat() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder()
.setFormat(CustomLog4j2StructuredLoggingFormatter.class.getName())
StructuredLogLayout layout = newBuilder().setFormat(CustomLog4j2StructuredLoggingFormatter.class.getName())
.build();
String format = layout.toSerializable(createEvent());
assertThat(format).isEqualTo("custom-format");
@ -61,40 +83,41 @@ class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
@Test
void shouldInjectCustomFormatConstructorParameters() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder()
StructuredLogLayout layout = newBuilder()
.setFormat(CustomLog4j2StructuredLoggingFormatterWithInjection.class.getName())
.setPid(1L)
.build();
String format = layout.toSerializable(createEvent());
assertThat(format).isEqualTo("custom-format-with-injection pid=1");
assertThat(format).isEqualTo("custom-format-with-injection pid=" + new ApplicationPid());
}
@Test
void shouldCheckTypeArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StructuredLogLayout.newBuilder()
.setFormat(CustomLog4j2StructuredLoggingFormatterWrongType.class.getName())
.build())
assertThatIllegalArgumentException().isThrownBy(
() -> newBuilder().setFormat(CustomLog4j2StructuredLoggingFormatterWrongType.class.getName()).build())
.withMessageContaining("must be org.apache.logging.log4j.core.LogEvent but was java.lang.String");
}
@Test
void shouldCheckTypeArgumentWithRawType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StructuredLogLayout.newBuilder()
.setFormat(CustomLog4j2StructuredLoggingFormatterRawType.class.getName())
.build())
.isThrownBy(
() -> newBuilder().setFormat(CustomLog4j2StructuredLoggingFormatterRawType.class.getName()).build())
.withMessageContaining("must be org.apache.logging.log4j.core.LogEvent but was null");
}
@Test
void shouldFailIfNoCommonOrCustomFormatIsSet() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StructuredLogLayout.newBuilder().setFormat("does-not-exist").build())
assertThatIllegalArgumentException().isThrownBy(() -> newBuilder().setFormat("does-not-exist").build())
.withMessageContaining("Unknown format 'does-not-exist'. "
+ "Values can be a valid fully-qualified class name or one of the common formats: [ecs, logstash]");
}
private Builder newBuilder() {
Builder builder = StructuredLogLayout.newBuilder();
ReflectionTestUtils.setField(builder, "loggerContext", this.loggerContext);
return builder;
}
static final class CustomLog4j2StructuredLoggingFormatter implements StructuredLogFormatter<LogEvent> {
@Override
@ -106,15 +129,15 @@ class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
static final class CustomLog4j2StructuredLoggingFormatterWithInjection implements StructuredLogFormatter<LogEvent> {
private final ApplicationMetadata metadata;
private final ApplicationPid pid;
CustomLog4j2StructuredLoggingFormatterWithInjection(ApplicationMetadata metadata) {
this.metadata = metadata;
CustomLog4j2StructuredLoggingFormatterWithInjection(ApplicationPid pid) {
this.pid = pid;
}
@Override
public String format(LogEvent event) {
return "custom-format-with-injection pid=" + this.metadata.pid();
return "custom-format-with-injection pid=" + this.pid;
}
}

View File

@ -24,7 +24,9 @@ import ch.qos.logback.classic.spi.ThrowableProxy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.ElasticCommonSchemaService;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.boot.system.MockApplicationPid;
import static org.assertj.core.api.Assertions.assertThat;
@ -33,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Moritz Halbritter
*/
class LogbackEcsStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private ElasticCommonSchemaStructuredLogFormatter formatter;
@ -41,8 +43,9 @@ class LogbackEcsStructuredLoggingFormatterTests extends AbstractStructuredLoggin
@BeforeEach
void setUp() {
super.setUp();
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(
new ApplicationMetadata(1L, "name", "1.0.0", "test", "node-1"), getThrowableProxyConverter());
ApplicationPid pid = MockApplicationPid.of(1L);
ElasticCommonSchemaService service = new ElasticCommonSchemaService("name", "1.0.0", "test", "node-1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(pid, service, getThrowableProxyConverter());
}
@Test
@ -70,10 +73,10 @@ class LogbackEcsStructuredLoggingFormatterTests extends AbstractStructuredLoggin
.containsAllEntriesOf(map("error.type", "java.lang.RuntimeException", "error.message", "Boom"));
String stackTrace = (String) deserialized.get("error.stack_trace");
assertThat(stackTrace).startsWith(
"java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback.LogbackEcsStructuredLoggingFormatterTests.shouldFormatException"
"java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException"
.formatted());
assertThat(json).contains(
"java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.logback.LogbackEcsStructuredLoggingFormatterTests.shouldFormatException"
"java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.logback.ElasticCommonSchemaStructuredLogFormatterTests.shouldFormatException"
.formatted()
.replace("\n", "\\n")
.replace("\r", "\\r"));

View File

@ -65,6 +65,7 @@ import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.test.util.ReflectionTestUtils;
@ -940,6 +941,21 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests {
assertThat(output).doesNotContain("\033[");
}
@Test
void getEnvironment() {
this.loggingSystem.beforeInitialize();
initialize(this.initializationContext, null, null);
assertThat(this.logger.getLoggerContext().getObject(Environment.class.getName())).isSameAs(this.environment);
}
@Test
void getEnvironmentWhenUsingFile() {
this.loggingSystem.beforeInitialize();
LogFile logFile = getLogFile(tmpDir() + "/example.log", null, false);
initialize(this.initializationContext, "classpath:logback-nondefault.xml", logFile);
assertThat(this.logger.getLoggerContext().getObject(Environment.class.getName())).isSameAs(this.environment);
}
private void initialize(LoggingInitializationContext context, String configLocation, LogFile logFile) {
this.loggingSystem.getSystemProperties((ConfigurableEnvironment) context.getEnvironment()).apply(logFile);
this.loggingSystem.beforeInitialize();

View File

@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Moritz Halbritter
*/
class LogbackLogstashStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests {
private LogstashStructuredLogFormatter formatter;
@ -74,10 +74,10 @@ class LogbackLogstashStructuredLoggingFormatterTests extends AbstractStructuredL
Map<String, Object> deserialized = deserialize(json);
String stackTrace = (String) deserialized.get("stack_trace");
assertThat(stackTrace).startsWith(
"java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback.LogbackLogstashStructuredLoggingFormatterTests.shouldFormatException"
"java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback.LogstashStructuredLogFormatterTests.shouldFormatException"
.formatted());
assertThat(json).contains(
"java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.logback.LogbackLogstashStructuredLoggingFormatterTests.shouldFormatException"
"java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.logback.LogstashStructuredLogFormatterTests.shouldFormatException"
.formatted()
.replace("\n", "\\n")
.replace("\r", "\\r"));

View File

@ -23,12 +23,16 @@ import java.util.Map;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.ContextBase;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.core.env.Environment;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -37,16 +41,25 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link StructuredLogEncoder}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class StructuredLoggingEncoderTests extends AbstractStructuredLoggingTests {
private StructuredLogEncoder encoder;
private Context loggerContext;
private MockEnvironment environment;
@Override
@BeforeEach
void setUp() {
super.setUp();
this.environment = new MockEnvironment();
this.loggerContext = new ContextBase();
this.loggerContext.putObject(Environment.class.getName(), this.environment);
this.encoder = new StructuredLogEncoder();
this.encoder.setContext(this.loggerContext);
}
@Override
@ -91,12 +104,12 @@ class StructuredLoggingEncoderTests extends AbstractStructuredLoggingTests {
@Test
void shouldInjectCustomFormatConstructorParameters() {
this.encoder.setFormat(CustomLogbackStructuredLoggingFormatterWithInjection.class.getName());
this.encoder.setPid(1L);
this.encoder.start();
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
String format = encode(event);
assertThat(format).isEqualTo("custom-format-with-injection pid=1 hasThrowableProxyConverter=true");
assertThat(format)
.isEqualTo("custom-format-with-injection pid=" + new ApplicationPid() + " hasThrowableProxyConverter=true");
}
@Test
@ -141,20 +154,20 @@ class StructuredLoggingEncoderTests extends AbstractStructuredLoggingTests {
static final class CustomLogbackStructuredLoggingFormatterWithInjection
implements StructuredLogFormatter<ILoggingEvent> {
private final ApplicationMetadata metadata;
private final ApplicationPid pid;
private final ThrowableProxyConverter throwableProxyConverter;
CustomLogbackStructuredLoggingFormatterWithInjection(ApplicationMetadata metadata,
CustomLogbackStructuredLoggingFormatterWithInjection(ApplicationPid pid,
ThrowableProxyConverter throwableProxyConverter) {
this.metadata = metadata;
this.pid = pid;
this.throwableProxyConverter = throwableProxyConverter;
}
@Override
public String format(ILoggingEvent event) {
boolean hasThrowableProxyConverter = this.throwableProxyConverter != null;
return "custom-format-with-injection pid=" + this.metadata.pid() + " hasThrowableProxyConverter="
return "custom-format-with-injection pid=" + this.pid + " hasThrowableProxyConverter="
+ hasThrowableProxyConverter;
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2012-2024 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 org.junit.jupiter.api.Test;
import org.springframework.boot.json.JsonWriter;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ElasticCommonSchemaService}.
*
* @author Phillip Webb
*/
class ElasticCommonSchemaServiceTests {
@Test
void getBindsFromEnvironment() {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("logging.structured.ecs.service.name", "spring");
environment.setProperty("logging.structured.ecs.service.version", "1.2.3");
environment.setProperty("logging.structured.ecs.service.environment", "prod");
environment.setProperty("logging.structured.ecs.service.node-name", "boot");
ElasticCommonSchemaService service = ElasticCommonSchemaService.get(environment);
assertThat(service).isEqualTo(new ElasticCommonSchemaService("spring", "1.2.3", "prod", "boot"));
}
@Test
void getWhenNoServiceNameUsesApplicationName() {
MockEnvironment environment = new MockEnvironment();
environment.setProperty("spring.application.name", "spring");
ElasticCommonSchemaService service = ElasticCommonSchemaService.get(environment);
assertThat(service).isEqualTo(new ElasticCommonSchemaService("spring", null, null, null));
}
@Test
void getWhenNoPropertiesToBind() {
MockEnvironment environment = new MockEnvironment();
ElasticCommonSchemaService service = ElasticCommonSchemaService.get(environment);
assertThat(service).isEqualTo(new ElasticCommonSchemaService(null, null, null, null));
}
@Test
void addToJsonMembersCreatesValidJson() {
ElasticCommonSchemaService service = new ElasticCommonSchemaService("spring", "1.2.3", "prod", "boot");
JsonWriter<ElasticCommonSchemaService> writer = JsonWriter.of(service::jsonMembers);
assertThat(writer.writeToString(service))
.isEqualTo("{\"service.name\":\"spring\",\"service.version\":\"1.2.3\","
+ "\"service.environment\":\"prod\",\"service.node.name\":\"boot\"}");
}
}

View File

@ -20,6 +20,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.core.env.Environment;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -31,13 +33,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/
class StructuredLogFormatterFactoryTests {
private final ApplicationMetadata applicationMetadata;
private final StructuredLogFormatterFactory<LogEvent> factory;
private final MockEnvironment environment = new MockEnvironment();
StructuredLogFormatterFactoryTests() {
this.applicationMetadata = new ApplicationMetadata(123L, "test", "1.2", null, null);
this.factory = new StructuredLogFormatterFactory<>(LogEvent.class, this.applicationMetadata,
this.environment.setProperty("logging.structured.ecs.service.version", "1.2.3");
this.factory = new StructuredLogFormatterFactory<>(LogEvent.class, this.environment,
this::addAvailableParameters, this::addCommonFormatters);
}
@ -47,7 +49,7 @@ class StructuredLogFormatterFactoryTests {
private void addCommonFormatters(CommonFormatters<LogEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new TestEcsFormatter(instantiator.getArg(ApplicationMetadata.class),
(instantiator) -> new TestEcsFormatter(instantiator.getArg(Environment.class),
instantiator.getArg(StringBuilder.class)));
}
@ -84,7 +86,7 @@ class StructuredLogFormatterFactoryTests {
@Test
void getUsingClassNameInjectsApplicationMetadata() {
TestEcsFormatter formatter = (TestEcsFormatter) this.factory.get(TestEcsFormatter.class.getName());
assertThat(formatter.getMetadata()).isSameAs(this.applicationMetadata);
assertThat(formatter.getEnvironment()).isSameAs(this.environment);
}
@Test
@ -103,22 +105,22 @@ class StructuredLogFormatterFactoryTests {
static class TestEcsFormatter implements StructuredLogFormatter<LogEvent> {
private ApplicationMetadata metadata;
private Environment environment;
private StringBuilder custom;
TestEcsFormatter(ApplicationMetadata metadata, StringBuilder custom) {
this.metadata = metadata;
TestEcsFormatter(Environment environment, StringBuilder custom) {
this.environment = environment;
this.custom = custom;
}
@Override
public String format(LogEvent event) {
return "formatted " + this.metadata.version();
return "formatted " + this.environment.getProperty("logging.structured.ecs.service.version");
}
ApplicationMetadata getMetadata() {
return this.metadata;
Environment getEnvironment() {
return this.environment;
}
StringBuilder getCustom() {
@ -129,8 +131,8 @@ class StructuredLogFormatterFactoryTests {
static class ExtendedTestEcsFormatter extends TestEcsFormatter {
ExtendedTestEcsFormatter(ApplicationMetadata metadata, StringBuilder custom) {
super(metadata, custom);
ExtendedTestEcsFormatter(Environment environment, StringBuilder custom) {
super(environment, custom);
}
}

View File

@ -19,23 +19,23 @@ package smoketest.structuredlogging.log4j2;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.system.ApplicationPid;
public class CustomStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
private final ApplicationMetadata metadata;
private final ApplicationPid pid;
public CustomStructuredLogFormatter(ApplicationMetadata metadata) {
this.metadata = metadata;
public CustomStructuredLogFormatter(ApplicationPid pid) {
this.pid = pid;
}
@Override
public String format(LogEvent event) {
StringBuilder result = new StringBuilder();
result.append("epoch=").append(event.getInstant().getEpochMillisecond());
if (this.metadata.pid() != null) {
result.append(" pid=").append(this.metadata.pid());
if (this.pid.isAvailable()) {
result.append(" pid=").append(this.pid);
}
result.append(" msg=\"").append(event.getMessage().getFormattedMessage()).append('"');
ThrowableProxy throwable = event.getThrownProxy();

View File

@ -20,17 +20,17 @@ import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.system.ApplicationPid;
public class CustomStructuredLogFormatter implements StructuredLogFormatter<ILoggingEvent> {
private final ApplicationPid pid;
private final ThrowableProxyConverter throwableProxyConverter;
private final ApplicationMetadata metadata;
public CustomStructuredLogFormatter(ApplicationMetadata metadata, ThrowableProxyConverter throwableProxyConverter) {
this.metadata = metadata;
public CustomStructuredLogFormatter(ApplicationPid pid, ThrowableProxyConverter throwableProxyConverter) {
this.pid = pid;
this.throwableProxyConverter = throwableProxyConverter;
}
@ -38,8 +38,8 @@ public class CustomStructuredLogFormatter implements StructuredLogFormatter<ILog
public String format(ILoggingEvent event) {
StringBuilder result = new StringBuilder();
result.append("epoch=").append(event.getInstant().toEpochMilli());
if (this.metadata.pid() != null) {
result.append(" pid=").append(this.metadata.pid());
if (this.pid.isAvailable()) {
result.append(" pid=").append(this.pid);
}
result.append(" msg=\"").append(event.getFormattedMessage()).append('"');
IThrowableProxy throwable = event.getThrowableProxy();