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.
|
* `defaults.xml` - Provides conversion rules, pattern properties and common logger configurations.
|
||||||
* `console-appender.xml` - Adds a `ConsoleAppender` using the `CONSOLE_LOG_PATTERN`.
|
* `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.
|
* `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.
|
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`
|
| `LOG_LEVEL_PATTERN`
|
||||||
| The format to use when rendering the log level (default `%5p`).
|
| 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`
|
||||||
| `PID`
|
| `PID`
|
||||||
| The current process ID (discovered if possible and when not already defined as an OS environment variable).
|
| 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]]
|
[[features.logging.logback-extensions]]
|
||||||
== 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.EXCEPTION_CONVERSION_WORD, resolver);
|
||||||
setSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN, resolver);
|
setSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN, resolver);
|
||||||
setSystemProperty(LoggingSystemProperty.FILE_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.LEVEL_PATTERN, resolver);
|
||||||
setSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN, resolver);
|
setSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN, resolver);
|
||||||
setSystemProperty(LoggingSystemProperty.CORRELATION_PATTERN, resolver);
|
setSystemProperty(LoggingSystemProperty.CORRELATION_PATTERN, resolver);
|
||||||
|
|
|
@ -85,6 +85,18 @@ public enum LoggingSystemProperty {
|
||||||
*/
|
*/
|
||||||
FILE_PATTERN("FILE_LOG_PATTERN", "logging.pattern.file"),
|
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.
|
* 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.classic.spi.ILoggingEvent;
|
||||||
import ch.qos.logback.core.Appender;
|
import ch.qos.logback.core.Appender;
|
||||||
import ch.qos.logback.core.ConsoleAppender;
|
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.RollingFileAppender;
|
||||||
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
|
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
|
||||||
import ch.qos.logback.core.spi.ScanException;
|
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.AnsiElement;
|
||||||
import org.springframework.boot.ansi.AnsiStyle;
|
import org.springframework.boot.ansi.AnsiStyle;
|
||||||
import org.springframework.boot.logging.LogFile;
|
import org.springframework.boot.logging.LogFile;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default logback configuration used by Spring Boot. Uses {@link LogbackConfigurator} to
|
* 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 Robert Thornton
|
||||||
* @author Scott Frederick
|
* @author Scott Frederick
|
||||||
* @author Jonatan Ivanov
|
* @author Jonatan Ivanov
|
||||||
|
* @author Moritz Halbritter
|
||||||
*/
|
*/
|
||||||
class DefaultLogbackConfiguration {
|
class DefaultLogbackConfiguration {
|
||||||
|
|
||||||
|
@ -99,9 +103,11 @@ class DefaultLogbackConfiguration {
|
||||||
putProperty(config, "CONSOLE_LOG_PATTERN", CONSOLE_LOG_PATTERN);
|
putProperty(config, "CONSOLE_LOG_PATTERN", CONSOLE_LOG_PATTERN);
|
||||||
putProperty(config, "CONSOLE_LOG_CHARSET", "${CONSOLE_LOG_CHARSET:-" + DEFAULT_CHARSET + "}");
|
putProperty(config, "CONSOLE_LOG_CHARSET", "${CONSOLE_LOG_CHARSET:-" + DEFAULT_CHARSET + "}");
|
||||||
putProperty(config, "CONSOLE_LOG_THRESHOLD", "${CONSOLE_LOG_THRESHOLD:-TRACE}");
|
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_PATTERN", FILE_LOG_PATTERN);
|
||||||
putProperty(config, "FILE_LOG_CHARSET", "${FILE_LOG_CHARSET:-" + DEFAULT_CHARSET + "}");
|
putProperty(config, "FILE_LOG_CHARSET", "${FILE_LOG_CHARSET:-" + DEFAULT_CHARSET + "}");
|
||||||
putProperty(config, "FILE_LOG_THRESHOLD", "${FILE_LOG_THRESHOLD:-TRACE}");
|
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.startup.DigesterFactory", Level.ERROR);
|
||||||
config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);
|
config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);
|
||||||
config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);
|
config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);
|
||||||
|
@ -123,36 +129,59 @@ class DefaultLogbackConfiguration {
|
||||||
|
|
||||||
private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {
|
private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {
|
||||||
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
|
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
|
||||||
ThresholdFilter filter = new ThresholdFilter();
|
createAppender(config, appender, "CONSOLE");
|
||||||
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);
|
|
||||||
config.appender("CONSOLE", appender);
|
config.appender("CONSOLE", appender);
|
||||||
return appender;
|
return appender;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Appender<ILoggingEvent> fileAppender(LogbackConfigurator config, String logFile) {
|
private Appender<ILoggingEvent> fileAppender(LogbackConfigurator config, String logFile) {
|
||||||
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
|
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
|
||||||
ThresholdFilter filter = new ThresholdFilter();
|
createAppender(config, appender, "FILE");
|
||||||
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);
|
|
||||||
appender.setFile(logFile);
|
appender.setFile(logFile);
|
||||||
setRollingPolicy(appender, config);
|
setRollingPolicy(appender, config);
|
||||||
config.appender("FILE", appender);
|
config.appender("FILE", appender);
|
||||||
return 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) {
|
private void setRollingPolicy(RollingFileAppender<ILoggingEvent> appender, LogbackConfigurator config) {
|
||||||
SizeAndTimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new SizeAndTimeBasedRollingPolicy<>();
|
SizeAndTimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new SizeAndTimeBasedRollingPolicy<>();
|
||||||
rollingPolicy.setContext(config.getContext());
|
rollingPolicy.setContext(config.getContext());
|
||||||
|
@ -176,6 +205,10 @@ class DefaultLogbackConfiguration {
|
||||||
return Integer.parseInt(resolve(config, val));
|
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) {
|
private FileSize resolveFileSize(LogbackConfigurator config, String val) {
|
||||||
return FileSize.valueOf(resolve(config, 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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)));
|
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.
|
* Instantiate the given set of classes, injecting constructor arguments as necessary.
|
||||||
* @param types the types to instantiate
|
* @param types the types to instantiate
|
||||||
|
@ -136,6 +159,22 @@ public class Instantiator<T> {
|
||||||
return instantiate(types.stream().map(TypeSupplier::forType));
|
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) {
|
private List<T> instantiate(Stream<TypeSupplier> typeSuppliers) {
|
||||||
return typeSuppliers.map(this::instantiate).sorted(AnnotationAwareOrderComparator.INSTANCE).toList();
|
return typeSuppliers.map(this::instantiate).sorted(AnnotationAwareOrderComparator.INSTANCE).toList();
|
||||||
}
|
}
|
||||||
|
@ -242,7 +281,7 @@ public class Instantiator<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<?> get() throws ClassNotFoundException {
|
public Class<?> get() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -223,6 +223,16 @@
|
||||||
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
|
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
|
||||||
"defaultValue": true
|
"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",
|
"name": "logging.threshold.console",
|
||||||
"type": "java.lang.String",
|
"type": "java.lang.String",
|
||||||
|
|
|
@ -9,13 +9,27 @@
|
||||||
</Properties>
|
</Properties>
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console" target="SYSTEM_OUT" follow="true">
|
<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}"/>
|
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
|
||||||
|
</DefaultArbiter>
|
||||||
|
</Select>
|
||||||
<Filters>
|
<Filters>
|
||||||
<ThresholdFilter level="${sys:CONSOLE_LOG_THRESHOLD:-TRACE}"/>
|
<ThresholdFilter level="${sys:CONSOLE_LOG_THRESHOLD:-TRACE}"/>
|
||||||
</Filters>
|
</Filters>
|
||||||
</Console>
|
</Console>
|
||||||
<RollingFile name="File" fileName="${sys:LOG_FILE}" filePattern="${sys:LOG_PATH}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">
|
<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}"/>
|
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}" charset="${sys:FILE_LOG_CHARSET}"/>
|
||||||
|
</DefaultArbiter>
|
||||||
|
</Select>
|
||||||
<Filters>
|
<Filters>
|
||||||
<ThresholdFilter level="${sys:FILE_LOG_THRESHOLD:-TRACE}"/>
|
<ThresholdFilter level="${sys:FILE_LOG_THRESHOLD:-TRACE}"/>
|
||||||
</Filters>
|
</Filters>
|
||||||
|
|
|
@ -9,7 +9,14 @@
|
||||||
</Properties>
|
</Properties>
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console" target="SYSTEM_OUT" follow="true">
|
<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}"/>
|
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" charset="${sys:CONSOLE_LOG_CHARSET}"/>
|
||||||
|
</DefaultArbiter>
|
||||||
|
</Select>
|
||||||
<filters>
|
<filters>
|
||||||
<ThresholdFilter level="${sys:CONSOLE_LOG_THRESHOLD:-TRACE}"/>
|
<ThresholdFilter level="${sys:CONSOLE_LOG_THRESHOLD:-TRACE}"/>
|
||||||
</filters>
|
</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_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_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
|
||||||
<property name="CONSOLE_LOG_THRESHOLD" value="${CONSOLE_LOG_THRESHOLD:-TRACE}"/>
|
<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_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_CHARSET" value="${FILE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
|
||||||
<property name="FILE_LOG_THRESHOLD" value="${FILE_LOG_THRESHOLD:-TRACE}"/>
|
<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.startup.DigesterFactory" level="ERROR"/>
|
||||||
<logger name="org.apache.catalina.util.LifecycleBase" 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");
|
.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) {
|
private Environment environment(String key, Object value) {
|
||||||
StandardEnvironment environment = new StandardEnvironment();
|
StandardEnvironment environment = new StandardEnvironment();
|
||||||
environment.getPropertySources().addLast(new MapPropertySource("test", Collections.singletonMap(key, value)));
|
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"));
|
.removeAll(Arrays.asList("LOG_FILE", "LOG_PATH", "LOGGED_APPLICATION_NAME", "LOGGED_APPLICATION_GROUP"));
|
||||||
expectedProperties.add("org.jboss.logging.provider");
|
expectedProperties.add("org.jboss.logging.provider");
|
||||||
expectedProperties.add("LOG_CORRELATION_PATTERN");
|
expectedProperties.add("LOG_CORRELATION_PATTERN");
|
||||||
|
expectedProperties.add("CONSOLE_LOG_STRUCTURED_FORMAT");
|
||||||
|
expectedProperties.add("FILE_LOG_STRUCTURED_FORMAT");
|
||||||
assertThat(properties).containsOnlyKeys(expectedProperties);
|
assertThat(properties).containsOnlyKeys(expectedProperties);
|
||||||
assertThat(properties).containsEntry("CONSOLE_LOG_CHARSET", Charset.defaultCharset().name());
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.springframework.boot.util;
|
package org.springframework.boot.util;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -61,7 +62,6 @@ class InstantiatorTests {
|
||||||
void instantiateWhenAdditionalConstructorPicksMostSuitable() {
|
void instantiateWhenAdditionalConstructorPicksMostSuitable() {
|
||||||
WithAdditionalConstructor instance = createInstance(WithAdditionalConstructor.class);
|
WithAdditionalConstructor instance = createInstance(WithAdditionalConstructor.class);
|
||||||
assertThat(instance).isInstanceOf(WithAdditionalConstructor.class);
|
assertThat(instance).isInstanceOf(WithAdditionalConstructor.class);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -122,8 +122,30 @@ class InstantiatorTests {
|
||||||
.withMessageContaining("custom failure handler message");
|
.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) {
|
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) {
|
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