Add support for structured logging

Update Logback and Log4j2 integrations to support structured logging.
Support for the ECS and Logstash JSON formats is provided out-of-the-box
and the `StructuredLogFormatter` interface may be used to if further
custom formats need to be supported.

Closes gh-5479

Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
This commit is contained in:
Moritz Halbritter 2024-07-15 11:24:51 +01:00 committed by Phillip Webb
parent 89f3052f6e
commit bf2950c045
47 changed files with 2699 additions and 26 deletions

View File

@ -51,7 +51,9 @@ The following files are provided under `org/springframework/boot/logging/logback
* `defaults.xml` - Provides conversion rules, pattern properties and common logger configurations.
* `console-appender.xml` - Adds a `ConsoleAppender` using the `CONSOLE_LOG_PATTERN`.
* `structured-console-appender.xml` - Adds a `ConsoleAppender` using structured logging in the `CONSOLE_LOG_STRUCTURED_FORMAT`.
* `file-appender.xml` - Adds a `RollingFileAppender` using the `FILE_LOG_PATTERN` and `ROLLING_FILE_NAME_PATTERN` with appropriate settings.
* `structured-file-appender.xml` - Adds a `RollingFileAppender` using the `ROLLING_FILE_NAME_PATTERN` with structured logging in the `FILE_LOG_STRUCTURED_FORMAT`.
In addition, a legacy `base.xml` file is provided for compatibility with earlier versions of Spring Boot.

View File

@ -366,6 +366,14 @@ The properties that are transferred are described in the following table:
| `LOG_LEVEL_PATTERN`
| The format to use when rendering the log level (default `%5p`).
| configprop:logging.structured.format.console[]
| `CONSOLE_LOG_STRUCTURED_FORMAT`
| The structured logging format to use for console logging.
| configprop:logging.structured.format.file[]
| `FILE_LOG_STRUCTURED_FORMAT`
| The structured logging format to use for file logging.
| `PID`
| `PID`
| The current process ID (discovered if possible and when not already defined as an OS environment variable).
@ -425,6 +433,94 @@ Handling authenticated request
[[features.logging.structured]]
== 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:
* xref:#features.logging.structured.ecs[Elastic Common Schema (ECS)]
* xref:#features.logging.structured.logstash[Logstash]
To enable structured logging, set the property configprop:logging.structured.format.console[] (for console output) or configprop:logging.structured.format.file[] (for file output) to the id of the format you want to use.
[[features.logging.structured.ecs]]
=== Elastic Common Schema
https://www.elastic.co/guide/en/ecs/8.11/ecs-reference.html[Elastic Common Schema] is a JSON based logging format.
To enable the Elastic Common Schema log format, set the appropriate `format` property to `ecs`:
[configprops,yaml]
----
logging:
structured:
format:
console: ecs
file: ecs
----
A log line looks like this:
[source,json]
----
{"@timestamp":"2024-01-01T10:15:00.067462556Z","log.level":"INFO","process.pid":39599,"process.thread.name":"main","service.name":"simple","log.logger":"org.example.Application","message":"No active profile set, falling back to 1 default profile: \"default\"","ecs.version":"8.11"}
----
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.
[[features.logging.structured.logstash]]
=== Logstash JSON format
The https://github.com/logfellow/logstash-logback-encoder?tab=readme-ov-file#standard-fields[Logstash JSON format] is a JSON based logging format.
To enable the Logstash JSON log format, set the appropriate `format` property to `logstash`:
[configprops,yaml]
----
logging:
structured:
format:
console: logstash
file: logstash
----
A log line looks like this:
[source,json]
----
{"@timestamp":"2024-01-01T10:15:00.111037681+02:00","@version":"1","message":"No active profile set, falling back to 1 default profile: \"default\"","logger_name":"org.example.Application","thread_name":"main","level":"INFO","level_value":20000}
----
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.
If you add https://www.slf4j.org/api/org/slf4j/Marker.html[markers], these will show up in a `tags` string array in the JSON.
[[features.logging.structured.custom-format]]
=== Custom Structured Logging formats
The structured logging support in Spring Boot is extensible, allowing you to define your own custom format.
To do this, implement the `StructuredLoggingFormatter` interface. The generic type argument has to be `ILoggingEvent` when using Logback and `LogEvent` when using Log4j2 (that means your implementation is tied to a specific logging system).
Your implementation is then called with the log event and returns the `String` to be logged, as seen in this example:
include-code::MyCustomFormat[]
As you can see in the example, you can return any format, it doesn't have to be JSON.
To enable your custom format, set the property configprop:logging.structured.format.console[] or configprop:logging.structured.format.file[] to the fully qualified class name of your implementation.
Your implementation can use some constructor parameters, which are injected automatically.
Please see the JavaDoc of xref:api:java/org/springframework/boot/logging/structured/StructuredLogFormatter.html[`StructuredLogFormatter`] for more details.
[[features.logging.logback-extensions]]
== Logback Extensions

View File

@ -0,0 +1,30 @@
/*
* 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.docs.features.logging.structured.customformat;
import ch.qos.logback.classic.spi.ILoggingEvent;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
class MyCustomFormat implements StructuredLogFormatter<ILoggingEvent> {
@Override
public String format(ILoggingEvent event) {
return "time=" + event.getInstant() + " level=" + event.getLevel() + " message=" + event.getMessage() + "\n";
}
}

View File

@ -127,6 +127,8 @@ public class LoggingSystemProperties {
setSystemProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD, resolver);
setSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN, resolver);
setSystemProperty(LoggingSystemProperty.FILE_PATTERN, resolver);
setSystemProperty(LoggingSystemProperty.CONSOLE_STRUCTURED_FORMAT, resolver);
setSystemProperty(LoggingSystemProperty.FILE_STRUCTURED_FORMAT, resolver);
setSystemProperty(LoggingSystemProperty.LEVEL_PATTERN, resolver);
setSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN, resolver);
setSystemProperty(LoggingSystemProperty.CORRELATION_PATTERN, resolver);

View File

@ -85,6 +85,18 @@ public enum LoggingSystemProperty {
*/
FILE_PATTERN("FILE_LOG_PATTERN", "logging.pattern.file"),
/**
* Logging system property for the console structured logging format.
* @since 3.4.0
*/
CONSOLE_STRUCTURED_FORMAT("CONSOLE_LOG_STRUCTURED_FORMAT", "logging.structured.format.console"),
/**
* Logging system property for the file structured logging format.
* @since 3.4.0
*/
FILE_STRUCTURED_FORMAT("FILE_LOG_STRUCTURED_FORMAT", "logging.structured.format.file"),
/**
* Logging system property for the log level pattern.
*/

View File

@ -0,0 +1,80 @@
/*
* 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.log4j2;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.logging.log4j.core.time.Instant;
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.StructuredLogFormatter;
import org.springframework.util.ObjectUtils;
/**
* Log4j2 {@link StructuredLogFormatter} for
* {@link CommonStructuredLogFormat#ELASTIC_COMMON_SCHEMA}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
private final JsonWriter<LogEvent> writer;
ElasticCommonSchemaStructuredLogFormatter(ApplicationMetadata metadata) {
this.writer = JsonWriter.<LogEvent>of((members) -> logEventJson(metadata, members)).withNewLineAtEnd();
}
private void logEventJson(ApplicationMetadata metadata, JsonWriter.Members<LogEvent> members) {
members.add("@timestamp", LogEvent::getInstant).as(this::asTimestamp);
members.add("log.level", LogEvent::getLevel).as(Level::name);
members.add("process.pid", metadata::pid).whenNotNull();
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();
members.add("log.logger", LogEvent::getLoggerName);
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
members.add(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));
members.add(LogEvent::getThrownProxy).whenNotNull().usingMembers((thrownProxyMembers) -> {
thrownProxyMembers.add("error.type", ThrowableProxy::getThrowable)
.whenNotNull()
.as(ObjectUtils::nullSafeClassName);
thrownProxyMembers.add("error.message", ThrowableProxy::getMessage);
thrownProxyMembers.add("error.stack_trace", ThrowableProxy::getExtendedStackTraceAsString);
});
members.add("ecs.version", "8.11");
}
private 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

@ -0,0 +1,96 @@
/*
* 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.log4j2;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.TreeSet;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.logging.log4j.core.time.Instant;
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.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.util.CollectionUtils;
/**
* Log4j2 {@link StructuredLogFormatter} for {@link CommonStructuredLogFormat#LOGSTASH}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class LogstashStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
private JsonWriter<LogEvent> writer;
LogstashStructuredLogFormatter() {
this.writer = JsonWriter.<LogEvent>of(this::logEventJson).withNewLineAtEnd();
}
private void logEventJson(JsonWriter.Members<LogEvent> members) {
members.add("@timestamp", LogEvent::getInstant).as(this::asTimestamp);
members.add("@version", "1");
members.add("message", LogEvent::getMessage).as(Message::getFormattedMessage);
members.add("logger_name", LogEvent::getLoggerName);
members.add("thread_name", LogEvent::getThreadName);
members.add("level", LogEvent::getLevel).as(Level::name);
members.add("level_value", LogEvent::getLevel).as(Level::intLevel);
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("stack_trace", LogEvent::getThrownProxy)
.whenNotNull()
.as(ThrowableProxy::getExtendedStackTraceAsString);
}
private 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) {
Set<String> result = new TreeSet<>();
addMarkers(result, marker);
return result;
}
private void addMarkers(Set<String> result, Marker marker) {
result.add(marker.getName());
if (marker.hasParents()) {
for (Marker parent : marker.getParents()) {
addMarkers(result, parent);
}
}
}
@Override
public String format(LogEvent event) {
return this.writer.writeToString(event);
}
}

View File

@ -0,0 +1,144 @@
/*
* 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.log4j2;
import java.nio.charset.Charset;
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.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.layout.AbstractStringLayout;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
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.util.Assert;
/**
* {@link Layout Log4j2 Layout} for structured logging.
*
* @author Moritz Halbritter
* @author Phillip Webb
* @see StructuredLogFormatter
*/
@Plugin(name = "StructuredLogLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE)
final class StructuredLogLayout extends AbstractStringLayout {
private final StructuredLogFormatter<LogEvent> formatter;
private StructuredLogLayout(Charset charset, StructuredLogFormatter<LogEvent> formatter) {
super(charset);
Assert.notNull(formatter, "Formatter must not be null");
this.formatter = formatter;
}
@Override
public String toSerializable(LogEvent event) {
return this.formatter.format(event);
}
@PluginBuilderFactory
static StructuredLogLayout.Builder newBuilder() {
return new StructuredLogLayout.Builder();
}
static final class Builder implements org.apache.logging.log4j.core.util.Builder<StructuredLogLayout> {
@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;
}
Builder setCharset(String charset) {
this.charset = charset;
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);
StructuredLogFormatter<LogEvent> formatter = new StructuredLogFormatterFactory<>(LogEvent.class,
applicationMetadata, null, this::addCommonFormatters)
.get(this.format);
return new StructuredLogLayout(charset, formatter);
}
private void addCommonFormatters(CommonFormatters<LogEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new ElasticCommonSchemaStructuredLogFormatter(
instantiator.getArg(ApplicationMetadata.class)));
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH,
(instantiator) -> new LogstashStructuredLogFormatter());
}
}
}

View File

@ -24,6 +24,8 @@ import ch.qos.logback.classic.filter.ThresholdFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.OutputStreamAppender;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.spi.ScanException;
@ -34,6 +36,7 @@ import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiElement;
import org.springframework.boot.ansi.AnsiStyle;
import org.springframework.boot.logging.LogFile;
import org.springframework.util.StringUtils;
/**
* Default logback configuration used by Spring Boot. Uses {@link LogbackConfigurator} to
@ -47,6 +50,7 @@ import org.springframework.boot.logging.LogFile;
* @author Robert Thornton
* @author Scott Frederick
* @author Jonatan Ivanov
* @author Moritz Halbritter
*/
class DefaultLogbackConfiguration {
@ -99,9 +103,11 @@ class DefaultLogbackConfiguration {
putProperty(config, "CONSOLE_LOG_PATTERN", CONSOLE_LOG_PATTERN);
putProperty(config, "CONSOLE_LOG_CHARSET", "${CONSOLE_LOG_CHARSET:-" + DEFAULT_CHARSET + "}");
putProperty(config, "CONSOLE_LOG_THRESHOLD", "${CONSOLE_LOG_THRESHOLD:-TRACE}");
putProperty(config, "CONSOLE_LOG_STRUCTURED_FORMAT", "${CONSOLE_LOG_STRUCTURED_FORMAT:-}");
putProperty(config, "FILE_LOG_PATTERN", FILE_LOG_PATTERN);
putProperty(config, "FILE_LOG_CHARSET", "${FILE_LOG_CHARSET:-" + DEFAULT_CHARSET + "}");
putProperty(config, "FILE_LOG_THRESHOLD", "${FILE_LOG_THRESHOLD:-TRACE}");
putProperty(config, "FILE_LOG_STRUCTURED_FORMAT", "${FILE_LOG_STRUCTURED_FORMAT:-}");
config.logger("org.apache.catalina.startup.DigesterFactory", Level.ERROR);
config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);
config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);
@ -123,36 +129,59 @@ class DefaultLogbackConfiguration {
private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
ThresholdFilter filter = new ThresholdFilter();
filter.setLevel(resolve(config, "${CONSOLE_LOG_THRESHOLD}"));
filter.start();
appender.addFilter(filter);
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setPattern(resolve(config, "${CONSOLE_LOG_PATTERN}"));
encoder.setCharset(resolveCharset(config, "${CONSOLE_LOG_CHARSET}"));
config.start(encoder);
appender.setEncoder(encoder);
createAppender(config, appender, "CONSOLE");
config.appender("CONSOLE", appender);
return appender;
}
private Appender<ILoggingEvent> fileAppender(LogbackConfigurator config, String logFile) {
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
ThresholdFilter filter = new ThresholdFilter();
filter.setLevel(resolve(config, "${FILE_LOG_THRESHOLD}"));
filter.start();
appender.addFilter(filter);
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setPattern(resolve(config, "${FILE_LOG_PATTERN}"));
encoder.setCharset(resolveCharset(config, "${FILE_LOG_CHARSET}"));
appender.setEncoder(encoder);
config.start(encoder);
createAppender(config, appender, "FILE");
appender.setFile(logFile);
setRollingPolicy(appender, config);
config.appender("FILE", appender);
return appender;
}
private void createAppender(LogbackConfigurator config, OutputStreamAppender<ILoggingEvent> appender, String type) {
appender.addFilter(createThresholdFilter(config, type));
Encoder<ILoggingEvent> encoder = createEncoder(config, type);
appender.setEncoder(encoder);
config.start(encoder);
}
private ThresholdFilter createThresholdFilter(LogbackConfigurator config, String type) {
ThresholdFilter filter = new ThresholdFilter();
filter.setLevel(resolve(config, "${" + type + "_LOG_THRESHOLD}"));
filter.start();
return filter;
}
private Encoder<ILoggingEvent> createEncoder(LogbackConfigurator config, String type) {
Charset charset = resolveCharset(config, "${" + type + "_LOG_CHARSET}");
String structuredLogFormat = resolve(config, "${" + type + "_LOG_STRUCTURED_FORMAT}");
if (StringUtils.hasLength(structuredLogFormat)) {
StructuredLogEncoder encoder = createStructuredLoggingEncoder(config, structuredLogFormat);
encoder.setCharset(charset);
return encoder;
}
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setCharset(charset);
encoder.setPattern(resolve(config, "${" + type + "_LOG_PATTERN}"));
return encoder;
}
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;
}
private void setRollingPolicy(RollingFileAppender<ILoggingEvent> appender, LogbackConfigurator config) {
SizeAndTimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new SizeAndTimeBasedRollingPolicy<>();
rollingPolicy.setContext(config.getContext());
@ -176,6 +205,10 @@ 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

@ -0,0 +1,80 @@
/*
* 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.logback;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
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.StructuredLogFormatter;
/**
* Logback {@link StructuredLogFormatter} for
* {@link CommonStructuredLogFormat#ELASTIC_COMMON_SCHEMA}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class ElasticCommonSchemaStructuredLogFormatter implements StructuredLogFormatter<ILoggingEvent> {
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
private JsonWriter<ILoggingEvent> writer;
ElasticCommonSchemaStructuredLogFormatter(ApplicationMetadata metadata,
ThrowableProxyConverter throwableProxyConverter) {
this.writer = JsonWriter
.<ILoggingEvent>of((members) -> loggingEventJson(metadata, throwableProxyConverter, members))
.withNewLineAtEnd();
}
private void loggingEventJson(ApplicationMetadata metadata, 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.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();
members.add("log.logger", ILoggingEvent::getLoggerName);
members.add("message", ILoggingEvent::getFormattedMessage);
members.addMapEntries(ILoggingEvent::getMDCPropertyMap);
members.add(ILoggingEvent::getKeyValuePairs)
.whenNotEmpty()
.usingExtractedPairs(Iterable::forEach, keyValuePairExtractor);
members.addSelf().whenNotNull(ILoggingEvent::getThrowableProxy).usingMembers((throwableMembers) -> {
throwableMembers.add("error.type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
throwableMembers.add("error.message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
throwableMembers.add("error.stack_trace", (event) -> throwableProxyConverter.convert(event));
});
members.add("ecs.version", "8.11");
}
@Override
public String format(ILoggingEvent event) {
return this.writer.writeToString(event);
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.logback;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import org.slf4j.Marker;
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.StructuredLogFormatter;
/**
* Logback {@link StructuredLogFormatter} for {@link CommonStructuredLogFormat#LOGSTASH}.
*
* @author Moritz Halbritter
* @author Phillip Webb
*/
class LogstashStructuredLogFormatter implements StructuredLogFormatter<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();
}
private void loggingEventJson(ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
members.add("@timestamp", ILoggingEvent::getInstant).as(this::asTimestamp);
members.add("@version", "1");
members.add("message", ILoggingEvent::getFormattedMessage);
members.add("logger_name", ILoggingEvent::getLoggerName);
members.add("thread_name", ILoggingEvent::getThreadName);
members.add("level", ILoggingEvent::getLevel);
members.add("level_value", ILoggingEvent::getLevel).as(Level::toInt);
members.addMapEntries(ILoggingEvent::getMDCPropertyMap);
members.add(ILoggingEvent::getKeyValuePairs)
.whenNotEmpty()
.usingExtractedPairs(Iterable::forEach, keyValuePairExtractor);
members.add("tags", ILoggingEvent::getMarkerList).whenNotNull().as(this::getMarkers).whenNotEmpty();
members.add("stack_trace", (event) -> event)
.whenNotNull(ILoggingEvent::getThrowableProxy)
.as(throwableProxyConverter::convert);
}
private 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) {
Set<String> result = new LinkedHashSet<>();
addMarkers(result, markers.iterator());
return result;
}
private void addMarkers(Set<String> result, Iterator<Marker> iterator) {
while (iterator.hasNext()) {
Marker marker = iterator.next();
result.add(marker.getName());
if (marker.hasReferences()) {
addMarkers(result, marker.iterator());
}
}
}
@Override
public String format(ILoggingEvent event) {
return this.writer.writeToString(event);
}
}

View File

@ -0,0 +1,141 @@
/*
* 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.logback;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
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.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.util.Assert;
/**
* {@link Encoder Logback encoder} for structured logging.
*
* @author Moritz Halbritter
* @author Phillip Webb
* @since 3.4.0
* @see StructuredLogFormatter
*/
public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter();
private String format;
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;
}
@Override
public void start() {
Assert.state(this.format != null, "Format has not been set");
this.formatter = createFormatter(this.format);
super.start();
this.throwableProxyConverter.start();
}
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)
.get(format);
}
private void addAvailableParameters(AvailableParameters availableParameters) {
availableParameters.add(ThrowableProxyConverter.class, this.throwableProxyConverter);
}
private void addCommonFormatters(CommonFormatters<ILoggingEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new ElasticCommonSchemaStructuredLogFormatter(
instantiator.getArg(ApplicationMetadata.class),
instantiator.getArg(ThrowableProxyConverter.class)));
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH, (instantiator) -> new LogstashStructuredLogFormatter(
instantiator.getArg(ThrowableProxyConverter.class)));
}
@Override
public void stop() {
this.throwableProxyConverter.stop();
super.stop();
}
@Override
public byte[] headerBytes() {
return null;
}
@Override
public byte[] encode(ILoggingEvent event) {
return this.formatter.format(event).getBytes(this.charset);
}
@Override
public byte[] footerBytes() {
return null;
}
}

View File

@ -0,0 +1,32 @@
/*
* 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,69 @@
/*
* 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;
/**
* Common structured log formats supported by Spring Boot.
*
* @author Moritz Halbritter
* @author Phillip Webb
* @since 3.4.0
*/
public enum CommonStructuredLogFormat {
/**
* <a href="https://www.elastic.co/guide/en/ecs/current/ecs-log.html">Elasic Common
* Schema</a> (ECS) log format.
*/
ELASTIC_COMMON_SCHEMA("ecs"),
/**
* The <a href=
* "https://github.com/logfellow/logstash-logback-encoder?tab=readme-ov-file#standard-fields">Logstash</a>
* log format.
*/
LOGSTASH("logstash");
private final String id;
CommonStructuredLogFormat(String id) {
this.id = id;
}
/**
* Return the ID for this format.
* @return the format identifier
*/
String getId() {
return this.id;
}
/**
* Find the {@link CommonStructuredLogFormat} for the given ID.
* @param id the format identifier
* @return the associated {@link CommonStructuredLogFormat} or {@code null}
*/
static CommonStructuredLogFormat forId(String id) {
for (CommonStructuredLogFormat candidate : values()) {
if (candidate.getId().equalsIgnoreCase(id)) {
return candidate;
}
}
return null;
}
}

View File

@ -0,0 +1,47 @@
/*
* 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 ch.qos.logback.classic.pattern.ThrowableProxyConverter;
/**
* 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>
* </ul>
* When using Logback, implementing classes can also use the following parameter types in
* the constructor:
* <ul>
* <li>{@link ThrowableProxyConverter}</li>
* </ul>
*
* @param <E> the log event type
* @author Moritz Halbritter
* @since 3.4.0
*/
public interface StructuredLogFormatter<E> {
/**
* Formats the given log event.
* @param event the log event to write
* @return the formatted log event
*/
String format(E event);
}

View File

@ -0,0 +1,162 @@
/*
* 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.util.Collection;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Consumer;
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.util.Assert;
/**
* Factory that can be used to create a fully instantiated {@link StructuredLogFormatter}
* for either a {@link CommonStructuredLogFormat#getId() common format} or a
* fully-qualified class name.
*
* @param <E> the log even type
* @author Moritz Halbritter
* @author Phillip Webb
* @since 3.4.0
* @see StructuredLogFormatter
*/
public class StructuredLogFormatterFactory<E> {
private static FailureHandler failureHandler = (type, implementationName, failure) -> {
if (!(failure instanceof ClassNotFoundException)) {
throw new IllegalArgumentException(
"Unable to instantiate " + implementationName + " [" + type.getName() + "]", failure);
}
};
private final Class<E> logEventType;
private final Instantiator<StructuredLogFormatter<E>> instantiator;
private final CommonFormatters<E> commonFormatters;
/**
* Create a new {@link StructuredLogFormatterFactory} instance.
* @param logEventType the log event type
* @param applicationMetadata an {@link ApplicationMetadata} instance for injection
* @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,
Consumer<AvailableParameters> availableParameters, Consumer<CommonFormatters<E>> commonFormatters) {
this.logEventType = logEventType;
this.instantiator = new Instantiator<>(StructuredLogFormatter.class, (allAvailableParameters) -> {
allAvailableParameters.add(ApplicationMetadata.class, applicationMetadata);
if (availableParameters != null) {
availableParameters.accept(allAvailableParameters);
}
}, failureHandler);
this.commonFormatters = new CommonFormatters<>();
commonFormatters.accept(this.commonFormatters);
}
/**
* Get a new {@link StructuredLogFormatter} instance for the specified format.
* @param format the format requested (either a {@link CommonStructuredLogFormat} ID
* or a fully-qualified class name)
* @return a new {@link StructuredLogFormatter} instance
* @throws IllegalArgumentException if the format is unknown
*/
public StructuredLogFormatter<E> get(String format) {
StructuredLogFormatter<E> formatter = this.commonFormatters.get(this.instantiator, format);
formatter = (formatter != null) ? formatter : getUsingClassName(format);
if (formatter != null) {
return formatter;
}
throw new IllegalArgumentException(
"Unknown format '%s'. Values can be a valid fully-qualified class name or one of the common formats: %s"
.formatted(format, this.commonFormatters.getCommonNames()));
}
private StructuredLogFormatter<E> getUsingClassName(String className) {
StructuredLogFormatter<E> formatter = this.instantiator.instantiate(className);
if (formatter != null) {
checkTypeArgument(formatter);
}
return formatter;
}
private void checkTypeArgument(Object formatter) {
Class<?> typeArgument = GenericTypeResolver.resolveTypeArgument(formatter.getClass(),
StructuredLogFormatter.class);
Assert.isTrue(this.logEventType.equals(typeArgument),
() -> "Type argument of %s must be %s but was %s".formatted(formatter.getClass().getName(),
this.logEventType.getName(), (typeArgument != null) ? typeArgument.getName() : "null"));
}
/**
* Callback used for configure the {@link CommonFormatterFactory} to use for a given
* {@link CommonStructuredLogFormat}.
*
* @param <E> the log event type
*/
public static class CommonFormatters<E> {
private final Map<CommonStructuredLogFormat, CommonFormatterFactory<E>> factories = new TreeMap<>();
/**
* Add the factory that should be used for the given
* {@link CommonStructuredLogFormat}.
* @param format the common structured log format
* @param factory the factory to use
*/
public void add(CommonStructuredLogFormat format, CommonFormatterFactory<E> factory) {
this.factories.put(format, factory);
}
Collection<String> getCommonNames() {
return this.factories.keySet().stream().map(CommonStructuredLogFormat::getId).toList();
}
StructuredLogFormatter<E> get(Instantiator<StructuredLogFormatter<E>> instantiator, String format) {
CommonStructuredLogFormat commonFormat = CommonStructuredLogFormat.forId(format);
CommonFormatterFactory<E> factory = (commonFormat != null) ? this.factories.get(commonFormat) : null;
return (factory != null) ? factory.createFormatter(instantiator) : null;
}
}
/**
* Factory used to create a {@link StructuredLogFormatter} for a given
* {@link CommonStructuredLogFormat}.
*
* @param <E> the log event type
*/
@FunctionalInterface
public interface CommonFormatterFactory<E> {
/**
* Create the {@link StructuredLogFormatter} instance.
* @param instantiator instantiator that can be used to obtain arguments
* @return a new {@link StructuredLogFormatter} instance
*/
StructuredLogFormatter<E> createFormatter(Instantiator<StructuredLogFormatter<E>> instantiator);
}
}

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Support for structured logging.
*/
package org.springframework.boot.logging.structured;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* 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.
@ -125,6 +125,29 @@ public class Instantiator<T> {
return instantiate(names.stream().map((name) -> TypeSupplier.forName(classLoader, name)));
}
/**
* Instantiate the given set of class name, injecting constructor arguments as
* necessary.
* @param name the class name to instantiate
* @return an instantiated instance
* @since 3.4.0
*/
public T instantiate(String name) {
return instantiate((ClassLoader) null, name);
}
/**
* Instantiate the given set of class name, injecting constructor arguments as
* necessary.
* @param classLoader the source classloader
* @param name the class name to instantiate
* @return an instantiated instance
* @since 3.4.0
*/
public T instantiate(ClassLoader classLoader, String name) {
return instantiate(TypeSupplier.forName(classLoader, name));
}
/**
* Instantiate the given set of classes, injecting constructor arguments as necessary.
* @param types the types to instantiate
@ -136,6 +159,22 @@ public class Instantiator<T> {
return instantiate(types.stream().map(TypeSupplier::forType));
}
/**
* Get an injectable argument instance for the given type. This method can be used
* when manually instantiating an object without reflection.
* @param <A> the argument type
* @param type the argument type
* @return the argument to inject or {@code null}
* @since 3.4.0
*/
@SuppressWarnings("unchecked")
public <A> A getArg(Class<A> type) {
Assert.notNull(type, "'type' must not be null");
Function<Class<?>, Object> parameter = getAvailableParameter(type);
Assert.isTrue(parameter != null, "Unknown argument type " + type.getName());
return (A) parameter.apply(this.type);
}
private List<T> instantiate(Stream<TypeSupplier> typeSuppliers) {
return typeSuppliers.map(this::instantiate).sorted(AnnotationAwareOrderComparator.INSTANCE).toList();
}
@ -242,7 +281,7 @@ public class Instantiator<T> {
}
@Override
public Class<?> get() throws ClassNotFoundException {
public Class<?> get() {
return type;
}

View File

@ -223,6 +223,16 @@
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
"defaultValue": true
},
{
"name": "logging.structured.format.console",
"type": "java.lang.String",
"description": "Structured logging format for output to the console. Must be either a format id or a fully qualified class name."
},
{
"name": "logging.structured.format.file",
"type": "java.lang.String",
"description": "Structured logging format for output to a file. Must be either a format id or a fully qualified class name."
},
{
"name": "logging.threshold.console",
"type": "java.lang.String",

View File

@ -9,13 +9,27 @@
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}" />
<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:-}"/>
</SystemPropertyArbiter>
<DefaultArbiter>
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
</DefaultArbiter>
</Select>
<Filters>
<ThresholdFilter level="${sys:CONSOLE_LOG_THRESHOLD:-TRACE}"/>
</Filters>
</Console>
<RollingFile name="File" fileName="${sys:LOG_FILE}" filePattern="${sys:LOG_PATH}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}" charset="${sys:FILE_LOG_CHARSET}"/>
<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:-}"/>
</SystemPropertyArbiter>
<DefaultArbiter>
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}" charset="${sys:FILE_LOG_CHARSET}"/>
</DefaultArbiter>
</Select>
<Filters>
<ThresholdFilter level="${sys:FILE_LOG_THRESHOLD:-TRACE}"/>
</Filters>

View File

@ -9,7 +9,14 @@
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
<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:-}"/>
</SystemPropertyArbiter>
<DefaultArbiter>
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
</DefaultArbiter>
</Select>
<filters>
<ThresholdFilter level="${sys:CONSOLE_LOG_THRESHOLD:-TRACE}"/>
</filters>

View File

@ -15,9 +15,11 @@ Default logback configuration provided for import
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}){} %clr(${PID:-}){magenta} %clr(--- %esb(){APPLICATION_NAME}%esb{APPLICATION_GROUP}[%15.15t] ${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
<property name="CONSOLE_LOG_THRESHOLD" value="${CONSOLE_LOG_THRESHOLD:-TRACE}"/>
<property name="CONSOLE_LOG_STRUCTURED_FORMAT" value="${CONSOLE_LOG_STRUCTURED_FORMAT:-}"/>
<property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:-} --- %esb(){APPLICATION_NAME}%esb{APPLICATION_GROUP}[%t] ${LOG_CORRELATION_PATTERN:-}%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="FILE_LOG_CHARSET" value="${FILE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
<property name="FILE_LOG_THRESHOLD" value="${FILE_LOG_THRESHOLD:-TRACE}"/>
<property name="FILE_LOG_STRUCTURED_FORMAT" value="${FILE_LOG_STRUCTURED_FORMAT:-}"/>
<logger name="org.apache.catalina.startup.DigesterFactory" level="ERROR"/>
<logger name="org.apache.catalina.util.LifecycleBase" level="ERROR"/>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Console appender with structured logging logback configuration provided for import,
equivalent to the programmatic initialization performed by Boot
-->
<included>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${CONSOLE_LOG_THRESHOLD}</level>
</filter>
<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

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
File appender with structured logging logback configuration provided for import,
equivalent to the programmatic initialization performed by Boot
-->
<included>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${FILE_LOG_THRESHOLD}</level>
</filter>
<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">
<fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
<cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-7}</maxHistory>
</rollingPolicy>
</appender>
</included>

View File

@ -197,6 +197,22 @@ class LoggingSystemPropertiesTests {
.isEqualTo("OFF");
}
@Test
void shouldSetFileStructuredLogging() {
new LoggingSystemProperties(new MockEnvironment().withProperty("logging.structured.format.file", "ecs"))
.apply(null);
assertThat(System.getProperty(LoggingSystemProperty.FILE_STRUCTURED_FORMAT.getEnvironmentVariableName()))
.isEqualTo("ecs");
}
@Test
void shouldSetConsoleStructuredLogging() {
new LoggingSystemProperties(new MockEnvironment().withProperty("logging.structured.format.console", "ecs"))
.apply(null);
assertThat(System.getProperty(LoggingSystemProperty.CONSOLE_STRUCTURED_FORMAT.getEnvironmentVariableName()))
.isEqualTo("ecs");
}
private Environment environment(String key, Object value) {
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addLast(new MapPropertySource("test", Collections.singletonMap(key, value)));

View File

@ -0,0 +1,74 @@
/*
* 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.log4j2;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.apache.logging.log4j.message.SimpleMessage;
import org.assertj.core.api.Assertions;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for structured formatting tests.
*
* @author Moritz Halbritter
*/
abstract class AbstractStructuredLoggingTests {
static final Instant EVENT_TIME = Instant.ofEpochMilli(1719910193000L);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
protected Map<String, Object> map(Object... values) {
assertThat(values.length).isEven();
Map<String, Object> result = new HashMap<>();
for (int i = 0; i < values.length; i += 2) {
result.put(values[i].toString(), values[i + 1]);
}
return result;
}
protected static MutableLogEvent createEvent() {
MutableLogEvent event = new MutableLogEvent();
event.setTimeMillis(EVENT_TIME.toEpochMilli());
event.setLevel(Level.INFO);
event.setThreadName("main");
event.setLoggerName("org.example.Test");
event.setMessage(new SimpleMessage("message"));
return event;
}
protected Map<String, Object> deserialize(String json) {
try {
return OBJECT_MAPPER.readValue(json, new TypeReference<>() {
});
}
catch (JsonProcessingException ex) {
Assertions.fail("Failed to deserialize JSON: " + json, ex);
return null;
}
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.log4j2;
import java.util.Map;
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
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 static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ElasticCommonSchemaStructuredLogFormatter}.
*
* @author Moritz Halbritter
*/
class Log4j2EcsStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
private ElasticCommonSchemaStructuredLogFormatter formatter;
@BeforeEach
void setUp() {
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(
new ApplicationMetadata(1L, "name", "1.0.0", "test", "node-1"));
}
@Test
void shouldFormat() {
MutableLogEvent event = createEvent();
event.setContextData(new JdkMapAdapterStringMap(Map.of("mdc-1", "mdc-v-1"), true));
String json = this.formatter.format(event);
assertThat(json).endsWith("\n");
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", "2024-07-02T08:49:53Z",
"log.level", "INFO", "process.pid", 1, "process.thread.name", "main", "service.name", "name",
"service.version", "1.0.0", "service.environment", "test", "service.node.name", "node-1", "log.logger",
"org.example.Test", "message", "message", "mdc-1", "mdc-v-1", "ecs.version", "8.11"));
}
@Test
void shouldFormatException() {
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized)
.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
\tat org.springframework.boot.logging.log4j2.Log4j2EcsStructuredLoggingFormatterTests.shouldFormatException""");
assertThat(json).contains(
"""
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.Log4j2EcsStructuredLoggingFormatterTests.shouldFormatException""");
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.log4j2;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.MarkerManager.Log4jMarker;
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
import org.apache.logging.log4j.core.impl.MutableLogEvent;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LogstashStructuredLogFormatter}.
*
* @author Moritz Halbritter
*/
class Log4j2LogstashStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
private LogstashStructuredLogFormatter formatter;
@BeforeEach
void setUp() {
this.formatter = new LogstashStructuredLogFormatter();
}
@Test
void shouldFormat() {
MutableLogEvent event = createEvent();
event.setContextData(new JdkMapAdapterStringMap(Map.of("mdc-1", "mdc-v-1"), true));
Log4jMarker marker1 = new Log4jMarker("marker-1");
marker1.addParents(new Log4jMarker("marker-2"));
event.setMarker(marker1);
String json = this.formatter.format(event);
assertThat(json).endsWith("\n");
Map<String, Object> deserialized = deserialize(json);
String timestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME
.format(OffsetDateTime.ofInstant(EVENT_TIME, ZoneId.systemDefault()));
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", timestamp, "@version", "1",
"message", "message", "logger_name", "org.example.Test", "thread_name", "main", "level", "INFO",
"level_value", 400, "mdc-1", "mdc-v-1", "tags", List.of("marker-1", "marker-2")));
}
@Test
void shouldFormatException() {
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
String stackTrace = (String) deserialized.get("stack_trace");
assertThat(stackTrace).startsWith(
"""
java.lang.RuntimeException: Boom
\tat org.springframework.boot.logging.log4j2.Log4j2LogstashStructuredLoggingFormatterTests.shouldFormatException""");
assertThat(json).contains(
"""
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.Log4j2LogstashStructuredLoggingFormatterTests.shouldFormatException""");
}
}

View File

@ -0,0 +1,141 @@
/*
* 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.log4j2;
import java.util.Map;
import org.apache.logging.log4j.core.LogEvent;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.ApplicationMetadata;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link StructuredLogLayout}.
*
* @author Moritz Halbritter
*/
class StructuredLoggingLayoutTests extends AbstractStructuredLoggingTests {
@Test
void shouldSupportEcsCommonFormat() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder().setFormat("ecs").build();
String json = layout.toSerializable(createEvent());
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("ecs.version");
}
@Test
void shouldSupportLogstashCommonFormat() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder().setFormat("logstash").build();
String json = layout.toSerializable(createEvent());
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("@version");
}
@Test
void shouldSupportCustomFormat() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder()
.setFormat(CustomLog4j2StructuredLoggingFormatter.class.getName())
.build();
String format = layout.toSerializable(createEvent());
assertThat(format).isEqualTo("custom-format");
}
@Test
void shouldInjectCustomFormatConstructorParameters() {
StructuredLogLayout layout = StructuredLogLayout.newBuilder()
.setFormat(CustomLog4j2StructuredLoggingFormatterWithInjection.class.getName())
.setPid(1L)
.build();
String format = layout.toSerializable(createEvent());
assertThat(format).isEqualTo("custom-format-with-injection pid=1");
}
@Test
void shouldCheckTypeArgument() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StructuredLogLayout.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())
.withMessageContaining("must be org.apache.logging.log4j.core.LogEvent but was null");
}
@Test
void shouldFailIfNoCommonOrCustomFormatIsSet() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StructuredLogLayout.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]");
}
static final class CustomLog4j2StructuredLoggingFormatter implements StructuredLogFormatter<LogEvent> {
@Override
public String format(LogEvent event) {
return "custom-format";
}
}
static final class CustomLog4j2StructuredLoggingFormatterWithInjection implements StructuredLogFormatter<LogEvent> {
private final ApplicationMetadata metadata;
CustomLog4j2StructuredLoggingFormatterWithInjection(ApplicationMetadata metadata) {
this.metadata = metadata;
}
@Override
public String format(LogEvent event) {
return "custom-format-with-injection pid=" + this.metadata.pid();
}
}
static final class CustomLog4j2StructuredLoggingFormatterWrongType implements StructuredLogFormatter<String> {
@Override
public String format(String event) {
return event;
}
}
@SuppressWarnings("rawtypes")
static final class CustomLog4j2StructuredLoggingFormatterRawType implements StructuredLogFormatter {
@Override
public String format(Object event) {
return "";
}
}
}

View File

@ -0,0 +1,114 @@
/*
* 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.logback;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
import ch.qos.logback.classic.spi.LoggingEvent;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.slf4j.Marker;
import org.slf4j.event.KeyValuePair;
import org.slf4j.helpers.BasicMarkerFactory;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for structured formatting tests.
*
* @author Moritz Halbritter
*/
abstract class AbstractStructuredLoggingTests {
static final Instant EVENT_TIME = Instant.ofEpochSecond(1719910193L);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private ThrowableProxyConverter throwableProxyConverter;
private BasicMarkerFactory markerFactory;
@BeforeEach
void setUp() {
this.markerFactory = new BasicMarkerFactory();
this.throwableProxyConverter = new ThrowableProxyConverter();
this.throwableProxyConverter.start();
}
@AfterEach
void tearDown() {
this.throwableProxyConverter.stop();
}
protected Marker getMarker(String name) {
return this.markerFactory.getDetachedMarker(name);
}
protected ThrowableProxyConverter getThrowableProxyConverter() {
return this.throwableProxyConverter;
}
protected Map<String, Object> map(Object... values) {
assertThat(values.length).isEven();
Map<String, Object> result = new HashMap<>();
for (int i = 0; i < values.length; i += 2) {
result.put(values[i].toString(), values[i + 1]);
}
return result;
}
protected List<KeyValuePair> keyValuePairs(Object... values) {
assertThat(values.length).isEven();
List<KeyValuePair> result = new ArrayList<>();
for (int i = 0; i < values.length; i += 2) {
result.add(new KeyValuePair(values[i].toString(), values[i + 1]));
}
return result;
}
protected static LoggingEvent createEvent() {
LoggingEvent event = new LoggingEvent();
event.setInstant(EVENT_TIME);
event.setLevel(Level.INFO);
event.setThreadName("main");
event.setLoggerName("org.example.Test");
event.setMessage("message");
return event;
}
protected Map<String, Object> deserialize(String json) {
try {
return OBJECT_MAPPER.readValue(json, new TypeReference<>() {
});
}
catch (JsonProcessingException ex) {
Assertions.fail("Failed to deserialize JSON: " + json, ex);
return null;
}
}
}

View File

@ -0,0 +1,81 @@
/*
* 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.logback;
import java.util.Collections;
import java.util.Map;
import ch.qos.logback.classic.spi.LoggingEvent;
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 static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ElasticCommonSchemaStructuredLogFormatter}.
*
* @author Moritz Halbritter
*/
class LogbackEcsStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
private ElasticCommonSchemaStructuredLogFormatter formatter;
@Override
@BeforeEach
void setUp() {
super.setUp();
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(
new ApplicationMetadata(1L, "name", "1.0.0", "test", "node-1"), getThrowableProxyConverter());
}
@Test
void shouldFormat() {
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Map.of("mdc-1", "mdc-v-1"));
event.setKeyValuePairs(keyValuePairs("kv-1", "kv-v-1"));
String json = this.formatter.format(event);
assertThat(json).endsWith("\n");
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", "2024-07-02T08:49:53Z",
"log.level", "INFO", "process.pid", 1, "process.thread.name", "main", "service.name", "name",
"service.version", "1.0.0", "service.environment", "test", "service.node.name", "node-1", "log.logger",
"org.example.Test", "message", "message", "mdc-1", "mdc-v-1", "kv-1", "kv-v-1", "ecs.version", "8.11"));
}
@Test
void shouldFormatException() {
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized)
.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
\tat org.springframework.boot.logging.logback.LogbackEcsStructuredLoggingFormatterTests.shouldFormatException""");
assertThat(json).contains(
"""
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.logback.LogbackEcsStructuredLoggingFormatterTests.shouldFormatException""");
}
}

View File

@ -558,6 +558,8 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests {
.removeAll(Arrays.asList("LOG_FILE", "LOG_PATH", "LOGGED_APPLICATION_NAME", "LOGGED_APPLICATION_GROUP"));
expectedProperties.add("org.jboss.logging.provider");
expectedProperties.add("LOG_CORRELATION_PATTERN");
expectedProperties.add("CONSOLE_LOG_STRUCTURED_FORMAT");
expectedProperties.add("FILE_LOG_STRUCTURED_FORMAT");
assertThat(properties).containsOnlyKeys(expectedProperties);
assertThat(properties).containsEntry("CONSOLE_LOG_CHARSET", Charset.defaultCharset().name());
}

View File

@ -0,0 +1,106 @@
/*
* 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.logback;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Marker;
import org.springframework.util.StopWatch;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LogstashStructuredLogFormatter}.
*
* @author Moritz Halbritter
*/
class LogbackLogstashStructuredLoggingFormatterTests extends AbstractStructuredLoggingTests {
private LogstashStructuredLogFormatter formatter;
@Override
@BeforeEach
void setUp() {
super.setUp();
this.formatter = new LogstashStructuredLogFormatter(getThrowableProxyConverter());
}
@Test
void shouldFormat() {
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Map.of("mdc-1", "mdc-v-1"));
event.setKeyValuePairs(keyValuePairs("kv-1", "kv-v-1"));
Marker marker1 = getMarker("marker-1");
marker1.add(getMarker("marker-2"));
event.addMarker(marker1);
String json = this.formatter.format(event);
assertThat(json).endsWith("\n");
Map<String, Object> deserialized = deserialize(json);
String timestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME
.format(OffsetDateTime.ofInstant(EVENT_TIME, ZoneId.systemDefault()));
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", timestamp, "@version", "1",
"message", "message", "logger_name", "org.example.Test", "thread_name", "main", "level", "INFO",
"level_value", 20000, "mdc-1", "mdc-v-1", "kv-1", "kv-v-1", "tags", List.of("marker-1", "marker-2")));
}
@Test
void shouldFormatException() {
LoggingEvent event = createEvent();
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
event.setMDCPropertyMap(Collections.emptyMap());
String json = this.formatter.format(event);
Map<String, Object> deserialized = deserialize(json);
String stackTrace = (String) deserialized.get("stack_trace");
assertThat(stackTrace).startsWith(
"""
java.lang.RuntimeException: Boom
\tat org.springframework.boot.logging.logback.LogbackLogstashStructuredLoggingFormatterTests.shouldFormatException""");
assertThat(json).contains(
"""
java.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.logback.LogbackLogstashStructuredLoggingFormatterTests.shouldFormatException""");
}
@Test
void benchmark() {
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Map.of("mdc-1", "mdc-v-1"));
event.setKeyValuePairs(keyValuePairs("kv-1", "kv-v-1"));
Marker marker1 = getMarker("marker-1");
marker1.add(getMarker("marker-2"));
event.addMarker(marker1);
System.out.println(this.formatter.format(event));
StopWatch stopWatch = new StopWatch();
stopWatch.start();
for (int i = 0; i < 1000000; i++) {
this.formatter.format(event);
}
stopWatch.stop();
System.out.println(stopWatch);
}
}

View File

@ -0,0 +1,182 @@
/*
* 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.logback;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
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 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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link StructuredLogEncoder}.
*
* @author Moritz Halbritter
*/
class StructuredLoggingEncoderTests extends AbstractStructuredLoggingTests {
private StructuredLogEncoder encoder;
@Override
@BeforeEach
void setUp() {
super.setUp();
this.encoder = new StructuredLogEncoder();
}
@Override
@AfterEach
void tearDown() {
super.tearDown();
this.encoder.stop();
}
@Test
void shouldSupportEcsCommonFormat() {
this.encoder.setFormat("ecs");
this.encoder.start();
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
String json = encode(event);
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("ecs.version");
}
@Test
void shouldSupportLogstashCommonFormat() {
this.encoder.setFormat("logstash");
this.encoder.start();
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
String json = encode(event);
Map<String, Object> deserialized = deserialize(json);
assertThat(deserialized).containsKey("@version");
}
@Test
void shouldSupportCustomFormat() {
this.encoder.setFormat(CustomLogbackStructuredLoggingFormatter.class.getName());
this.encoder.start();
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
String format = encode(event);
assertThat(format).isEqualTo("custom-format");
}
@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");
}
@Test
void shouldCheckTypeArgument() {
assertThatIllegalArgumentException().isThrownBy(() -> {
this.encoder.setFormat(CustomLogbackStructuredLoggingFormatterWrongType.class.getName());
this.encoder.start();
}).withMessageContaining("must be ch.qos.logback.classic.spi.ILoggingEvent but was java.lang.String");
}
@Test
void shouldCheckTypeArgumentWithRawType() {
assertThatIllegalArgumentException().isThrownBy(() -> {
this.encoder.setFormat(CustomLogbackStructuredLoggingFormatterRawType.class.getName());
this.encoder.start();
}).withMessageContaining("must be ch.qos.logback.classic.spi.ILoggingEvent but was null");
}
@Test
void shouldFailIfNoCommonOrCustomFormatIsSet() {
assertThatIllegalArgumentException().isThrownBy(() -> {
this.encoder.setFormat("does-not-exist");
this.encoder.start();
})
.withMessageContaining(
"Unknown format 'does-not-exist'. Values can be a valid fully-qualified class name or one of the common formats: [ecs, logstash]");
}
private String encode(LoggingEvent event) {
return new String(this.encoder.encode(event), StandardCharsets.UTF_8);
}
static final class CustomLogbackStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {
@Override
public String format(ILoggingEvent event) {
return "custom-format";
}
}
static final class CustomLogbackStructuredLoggingFormatterWithInjection
implements StructuredLogFormatter<ILoggingEvent> {
private final ApplicationMetadata metadata;
private final ThrowableProxyConverter throwableProxyConverter;
CustomLogbackStructuredLoggingFormatterWithInjection(ApplicationMetadata metadata,
ThrowableProxyConverter throwableProxyConverter) {
this.metadata = metadata;
this.throwableProxyConverter = throwableProxyConverter;
}
@Override
public String format(ILoggingEvent event) {
boolean hasThrowableProxyConverter = this.throwableProxyConverter != null;
return "custom-format-with-injection pid=" + this.metadata.pid() + " hasThrowableProxyConverter="
+ hasThrowableProxyConverter;
}
}
static final class CustomLogbackStructuredLoggingFormatterWrongType implements StructuredLogFormatter<String> {
@Override
public String format(String event) {
return event;
}
}
@SuppressWarnings("rawtypes")
static final class CustomLogbackStructuredLoggingFormatterRawType implements StructuredLogFormatter {
@Override
public String format(Object event) {
return "";
}
}
}

View File

@ -0,0 +1,47 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CommonStructuredLogFormat}.
*
* @author Phillip Webb
*/
class CommonStructuredLogFormatTests {
@Test
void forIdReturnsCommonStructuredLogFormat() {
assertThat(CommonStructuredLogFormat.forId("ecs")).isEqualTo(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA);
assertThat(CommonStructuredLogFormat.forId("logstash")).isEqualTo(CommonStructuredLogFormat.LOGSTASH);
}
@Test
void forIdWhenIdIsInDifferentCaseReturnsCommonStructuredLogFormat() {
assertThat(CommonStructuredLogFormat.forId("ECS")).isEqualTo(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA);
assertThat(CommonStructuredLogFormat.forId("logSTAsh")).isEqualTo(CommonStructuredLogFormat.LOGSTASH);
}
@Test
void forIdWhenNotKnownReturnsNull() {
assertThat(CommonStructuredLogFormat.forId("madeup")).isNull();
}
}

View File

@ -0,0 +1,147 @@
/*
* 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.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link StructuredLogFormatterFactory}.
*
* @author Phillip Webb
*/
class StructuredLogFormatterFactoryTests {
private final ApplicationMetadata applicationMetadata;
private final StructuredLogFormatterFactory<LogEvent> factory;
StructuredLogFormatterFactoryTests() {
this.applicationMetadata = new ApplicationMetadata(123L, "test", "1.2", null, null);
this.factory = new StructuredLogFormatterFactory<>(LogEvent.class, this.applicationMetadata,
this::addAvailableParameters, this::addCommonFormatters);
}
private void addAvailableParameters(AvailableParameters availableParameters) {
availableParameters.add(StringBuilder.class, new StringBuilder("Hello"));
}
private void addCommonFormatters(CommonFormatters<LogEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new TestEcsFormatter(instantiator.getArg(ApplicationMetadata.class),
instantiator.getArg(StringBuilder.class)));
}
@Test
void getUsingCommonFormat() {
assertThat(this.factory.get("ecs")).isInstanceOf(TestEcsFormatter.class);
}
@Test
void getUsingClassName() {
assertThat(this.factory.get(ExtendedTestEcsFormatter.class.getName()))
.isInstanceOf(ExtendedTestEcsFormatter.class);
}
@Test
void getUsingClassNameWhenNoSuchClass() {
assertThatIllegalArgumentException()
.isThrownBy(() -> assertThat(this.factory.get("com.example.WeMadeItUp")).isNull())
.withMessage("Unknown format 'com.example.WeMadeItUp'. "
+ "Values can be a valid fully-qualified class name or one of the common formats: [ecs]");
}
@Test
void getUsingClassNameWhenHasGenericMismatch() {
assertThatIllegalArgumentException().isThrownBy(() -> this.factory.get(DifferentFormatter.class.getName()))
.withMessage("Type argument of org.springframework.boot.logging.structured."
+ "StructuredLogFormatterFactoryTests$DifferentFormatter "
+ "must be org.springframework.boot.logging.structured."
+ "StructuredLogFormatterFactoryTests$LogEvent "
+ "but was org.springframework.boot.logging.structured."
+ "StructuredLogFormatterFactoryTests$DifferentLogEvent");
}
@Test
void getUsingClassNameInjectsApplicationMetadata() {
TestEcsFormatter formatter = (TestEcsFormatter) this.factory.get(TestEcsFormatter.class.getName());
assertThat(formatter.getMetadata()).isSameAs(this.applicationMetadata);
}
@Test
void getUsingClassNameInjectsCustomParameter() {
TestEcsFormatter formatter = (TestEcsFormatter) this.factory.get(TestEcsFormatter.class.getName());
assertThat(formatter.getCustom()).hasToString("Hello");
}
static class LogEvent {
}
static class DifferentLogEvent {
}
static class TestEcsFormatter implements StructuredLogFormatter<LogEvent> {
private ApplicationMetadata metadata;
private StringBuilder custom;
TestEcsFormatter(ApplicationMetadata metadata, StringBuilder custom) {
this.metadata = metadata;
this.custom = custom;
}
@Override
public String format(LogEvent event) {
return "formatted " + this.metadata.version();
}
ApplicationMetadata getMetadata() {
return this.metadata;
}
StringBuilder getCustom() {
return this.custom;
}
}
static class ExtendedTestEcsFormatter extends TestEcsFormatter {
ExtendedTestEcsFormatter(ApplicationMetadata metadata, StringBuilder custom) {
super(metadata, custom);
}
}
static class DifferentFormatter implements StructuredLogFormatter<DifferentLogEvent> {
@Override
public String format(DifferentLogEvent event) {
return "";
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* 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.
@ -16,6 +16,7 @@
package org.springframework.boot.util;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -61,7 +62,6 @@ class InstantiatorTests {
void instantiateWhenAdditionalConstructorPicksMostSuitable() {
WithAdditionalConstructor instance = createInstance(WithAdditionalConstructor.class);
assertThat(instance).isInstanceOf(WithAdditionalConstructor.class);
}
@Test
@ -122,8 +122,30 @@ class InstantiatorTests {
.withMessageContaining("custom failure handler message");
}
@Test
void instantiateWithSingleNameCreatesInstance() {
WithDefaultConstructor instance = createInstantiator(WithDefaultConstructor.class)
.instantiate(WithDefaultConstructor.class.getName());
assertThat(instance).isInstanceOf(WithDefaultConstructor.class);
}
@Test
void getArgReturnsArg() {
Instantiator<?> instantiator = createInstantiator(WithMultipleConstructors.class);
assertThat(instantiator.getArg(ParamA.class)).isSameAs(this.paramA);
assertThat(instantiator.getArg(ParamB.class)).isSameAs(this.paramB);
assertThat(instantiator.getArg(ParamC.class)).isInstanceOf(ParamC.class);
}
@Test
void getArgWhenUnknownThrowsException() {
Instantiator<?> instantiator = createInstantiator(WithMultipleConstructors.class);
assertThatIllegalArgumentException().isThrownBy(() -> instantiator.getArg(InputStream.class))
.withMessageStartingWith("Unknown argument type");
}
private <T> T createInstance(Class<T> type) {
return createInstantiator(type).instantiate(Collections.singleton(type.getName())).get(0);
return createInstantiator(type).instantiate(type.getName());
}
private <T> Instantiator<T> createInstantiator(Class<T> type) {

View File

@ -0,0 +1,17 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot structure logging Log4j2 smoke test"
configurations.all {
exclude module: "spring-boot-starter-logging"
}
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-log4j2"))
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
}

View File

@ -0,0 +1,49 @@
/*
* 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 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;
public class CustomStructuredLogFormatter implements StructuredLogFormatter<LogEvent> {
private final ApplicationMetadata metadata;
public CustomStructuredLogFormatter(ApplicationMetadata metadata) {
this.metadata = metadata;
}
@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());
}
result.append(" msg=\"").append(event.getMessage().getFormattedMessage()).append('"');
ThrowableProxy throwable = event.getThrownProxy();
if (throwable != null) {
result.append(" error=\"").append(throwable.getExtendedStackTraceAsString()).append('"');
}
result.append('\n');
return result.toString();
}
}

View File

@ -0,0 +1,29 @@
/*
* 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 smoketest.structuredlogging.log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleLog4j2StructuredLoggingApplication {
public static void main(String[] args) {
SpringApplication.run(SampleLog4j2StructuredLoggingApplication.class, args);
}
}

View File

@ -0,0 +1,5 @@
spring.main.banner-mode=off
logging.structured.format.console=ecs
#---
spring.config.activate.on-profile=custom
logging.structured.format.console=smoketest.structuredlogging.log4j2.CustomStructuredLogFormatter

View File

@ -0,0 +1,59 @@
/*
* 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 smoketest.structuredlogging.log4j2;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemProperty;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SampleLog4j2StructuredLoggingApplication}.
*
* @author Phillip Webb
*/
@ExtendWith(OutputCaptureExtension.class)
class SampleLog4j2StructuredLoggingApplicationTests {
@AfterEach
void reset() {
LoggingSystem.get(getClass().getClassLoader()).cleanUp();
for (LoggingSystemProperty property : LoggingSystemProperty.values()) {
System.getProperties().remove(property.getEnvironmentVariableName());
}
}
@Test
void json(CapturedOutput output) {
SampleLog4j2StructuredLoggingApplication.main(new String[0]);
assertThat(output).contains("{\"@timestamp\"")
.contains("\"message\":\"Starting SampleLog4j2StructuredLoggingApplication");
}
@Test
void custom(CapturedOutput output) {
SampleLog4j2StructuredLoggingApplication.main(new String[] { "--spring.profiles.active=custom" });
assertThat(output).contains("epoch=").contains("msg=\"Starting SampleLog4j2StructuredLoggingApplication");
}
}

View File

@ -0,0 +1,12 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot structure logging smoke test"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
}

View File

@ -0,0 +1,53 @@
/*
* 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 smoketest.structuredlogging;
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;
public class CustomStructuredLogFormatter implements StructuredLogFormatter<ILoggingEvent> {
private final ThrowableProxyConverter throwableProxyConverter;
private final ApplicationMetadata metadata;
public CustomStructuredLogFormatter(ApplicationMetadata metadata, ThrowableProxyConverter throwableProxyConverter) {
this.metadata = metadata;
this.throwableProxyConverter = throwableProxyConverter;
}
@Override
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());
}
result.append(" msg=\"").append(event.getFormattedMessage()).append('"');
IThrowableProxy throwable = event.getThrowableProxy();
if (throwable != null) {
result.append(" error=\"").append(this.throwableProxyConverter.convert(event)).append('"');
}
result.append('\n');
return result.toString();
}
}

View File

@ -0,0 +1,29 @@
/*
* 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 smoketest.structuredlogging;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleStructuredLoggingApplication {
public static void main(String[] args) {
SpringApplication.run(SampleStructuredLoggingApplication.class, args);
}
}

View File

@ -0,0 +1,5 @@
spring.main.banner-mode=off
logging.structured.format.console=ecs
#---
spring.config.activate.on-profile=custom
logging.structured.format.console=smoketest.structuredlogging.CustomStructuredLogFormatter

View File

@ -0,0 +1,59 @@
/*
* 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 smoketest.structuredlogging;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemProperty;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SampleStructuredLoggingApplication}.
*
* @author Phillip Webb
*/
@ExtendWith(OutputCaptureExtension.class)
class SampleStructuredLoggingApplicationTests {
@AfterEach
void reset() {
LoggingSystem.get(getClass().getClassLoader()).cleanUp();
for (LoggingSystemProperty property : LoggingSystemProperty.values()) {
System.getProperties().remove(property.getEnvironmentVariableName());
}
}
@Test
void json(CapturedOutput output) {
SampleStructuredLoggingApplication.main(new String[0]);
assertThat(output).contains("{\"@timestamp\"")
.contains("\"message\":\"Starting SampleStructuredLoggingApplication");
}
@Test
void custom(CapturedOutput output) {
SampleStructuredLoggingApplication.main(new String[] { "--spring.profiles.active=custom" });
assertThat(output).contains("epoch=").contains("msg=\"Starting SampleStructuredLoggingApplication");
}
}