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:
parent
89f3052f6e
commit
bf2950c045
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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">
|
||||
<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>
|
||||
|
|
|
@ -9,7 +9,14 @@
|
|||
</Properties>
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT" follow="true">
|
||||
<Select>
|
||||
<SystemPropertyArbiter propertyName="CONSOLE_LOG_STRUCTURED_FORMAT">
|
||||
<StructuredLogLayout format="${sys:CONSOLE_LOG_STRUCTURED_FORMAT}" charset="${sys:CONSOLE_LOG_CHARSET}" pid="${sys:PID:--1}" serviceName="${sys:APPLICATION_NAME:-}"/>
|
||||
</SystemPropertyArbiter>
|
||||
<DefaultArbiter>
|
||||
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
|
||||
</DefaultArbiter>
|
||||
</Select>
|
||||
<filters>
|
||||
<ThresholdFilter level="${sys:CONSOLE_LOG_THRESHOLD:-TRACE}"/>
|
||||
</filters>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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""");
|
||||
}
|
||||
|
||||
}
|
|
@ -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""");
|
||||
}
|
||||
|
||||
}
|
|
@ -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 "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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""");
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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"))
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
|
@ -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"))
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue