Allow structure logging JSON to be customized
Introduce a new `StructureLoggingJsonMembersCustomizer` interface as well as additional properties that can be used to customize the JSON produced with structured logging. Closes gh-42486
This commit is contained in:
@@ -572,8 +572,50 @@ If you add https://www.slf4j.org/api/org/slf4j/Marker.html[markers], these will
|
||||
|
||||
|
||||
|
||||
[[features.logging.structured.custom-format]]
|
||||
=== Custom Structured Logging formats
|
||||
[[features.logging.structured.customizing-json]]
|
||||
=== Customizing Structured Logging JSON
|
||||
|
||||
Spring Boot attempts to pick sensible defaults for the JSON names and values output for structured logging.
|
||||
Sometimes, however, you may want to make small adjustments to the JSON for your own needs.
|
||||
For example, it's possible that you might want to change some of the names to match the expectations of your log ingestion system.
|
||||
You might also want to filter out certain members since you don't find them useful.
|
||||
|
||||
The following properties allow you to change the way that structured logging JSON is written:
|
||||
|
||||
|===
|
||||
| Property | Description
|
||||
|
||||
| configprop:logging.structured.json.include[] & configprop:logging.structured.json.exclude[]
|
||||
| Filters specific paths from the JSON
|
||||
|
||||
| configprop:logging.structured.json.rename[]
|
||||
| Renames a specific member in the JSON
|
||||
|
||||
| configprop:logging.structured.json.add[]
|
||||
| Adds additional members to the JSON
|
||||
|===
|
||||
|
||||
For example, the following will exclude `log.level`, rename `process.id` to `procid` and add a fixed `corpname` field:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
logging:
|
||||
structured:
|
||||
json:
|
||||
exclude: log.level
|
||||
rename:
|
||||
process.id: procid
|
||||
add:
|
||||
corpname: mycorp
|
||||
----
|
||||
|
||||
TIP: For more advanced customizations, you can write your own class that implements the javadoc:org.springframework.boot.logging.structured.StructuredLogFormatter[] interface and declare it using the configprop:logging.structured.json.customizer[] property.
|
||||
You can also declare implementations by listing them in a `META-INF/spring.factories` file.
|
||||
|
||||
|
||||
|
||||
[[features.logging.structured.other-formats]]
|
||||
=== Supporting Other 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).
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.features.logging.structured.customformat;
|
||||
package org.springframework.boot.docs.features.logging.structured.otherformats;
|
||||
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -41,8 +42,9 @@ import org.springframework.util.ObjectUtils;
|
||||
*/
|
||||
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
|
||||
|
||||
ElasticCommonSchemaStructuredLogFormatter(Environment environment) {
|
||||
super((members) -> jsonMembers(environment, members));
|
||||
ElasticCommonSchemaStructuredLogFormatter(Environment environment,
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super((members) -> jsonMembers(environment, members), customizer);
|
||||
}
|
||||
|
||||
private static void jsonMembers(Environment environment, JsonWriter.Members<LogEvent> members) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.boot.json.WritableJson;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
@@ -68,8 +69,9 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
|
||||
*/
|
||||
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id");
|
||||
|
||||
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment) {
|
||||
super((members) -> jsonMembers(environment, members));
|
||||
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment,
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super((members) -> jsonMembers(environment, members), customizer);
|
||||
}
|
||||
|
||||
private static void jsonMembers(Environment environment, JsonWriter.Members<LogEvent> members) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
|
||||
import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -43,8 +44,8 @@ import org.springframework.util.CollectionUtils;
|
||||
*/
|
||||
class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
|
||||
|
||||
LogstashStructuredLogFormatter() {
|
||||
super(LogstashStructuredLogFormatter::jsonMembers);
|
||||
LogstashStructuredLogFormatter(StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super(LogstashStructuredLogFormatter::jsonMembers, customizer);
|
||||
}
|
||||
|
||||
private static void jsonMembers(JsonWriter.Members<LogEvent> members) {
|
||||
|
||||
@@ -30,9 +30,11 @@ import org.apache.logging.log4j.core.config.plugins.PluginLoggerContext;
|
||||
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
|
||||
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
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;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -102,14 +104,29 @@ final class StructuredLogLayout extends AbstractStringLayout {
|
||||
}
|
||||
|
||||
private void addCommonFormatters(CommonFormatters<LogEvent> commonFormatters) {
|
||||
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
|
||||
(instantiator) -> new ElasticCommonSchemaStructuredLogFormatter(
|
||||
instantiator.getArg(Environment.class)));
|
||||
commonFormatters.add(CommonStructuredLogFormat.GRAYLOG_EXTENDED_LOG_FORMAT,
|
||||
(instantiator) -> new GraylogExtendedLogFormatStructuredLogFormatter(
|
||||
instantiator.getArg(Environment.class)));
|
||||
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH,
|
||||
(instantiator) -> new LogstashStructuredLogFormatter());
|
||||
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA, this::createEcsFormatter);
|
||||
commonFormatters.add(CommonStructuredLogFormat.GRAYLOG_EXTENDED_LOG_FORMAT, this::createGraylogFormatter);
|
||||
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH, this::createLogstashFormatter);
|
||||
}
|
||||
|
||||
private ElasticCommonSchemaStructuredLogFormatter createEcsFormatter(Instantiator<?> instantiator) {
|
||||
Environment environment = instantiator.getArg(Environment.class);
|
||||
StructureLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
|
||||
.getArg(StructureLoggingJsonMembersCustomizer.class);
|
||||
return new ElasticCommonSchemaStructuredLogFormatter(environment, jsonMembersCustomizer);
|
||||
}
|
||||
|
||||
private GraylogExtendedLogFormatStructuredLogFormatter createGraylogFormatter(Instantiator<?> instantiator) {
|
||||
Environment environment = instantiator.getArg(Environment.class);
|
||||
StructureLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
|
||||
.getArg(StructureLoggingJsonMembersCustomizer.class);
|
||||
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, jsonMembersCustomizer);
|
||||
}
|
||||
|
||||
private LogstashStructuredLogFormatter createLogstashFormatter(Instantiator<?> instantiator) {
|
||||
StructureLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
|
||||
.getArg(StructureLoggingJsonMembersCustomizer.class);
|
||||
return new LogstashStructuredLogFormatter(jsonMembersCustomizer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.springframework.boot.json.JsonWriter.PairExtractor;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
@@ -43,9 +44,9 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
|
||||
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
|
||||
(pair) -> pair.value);
|
||||
|
||||
ElasticCommonSchemaStructuredLogFormatter(Environment environment,
|
||||
ThrowableProxyConverter throwableProxyConverter) {
|
||||
super((members) -> jsonMembers(environment, throwableProxyConverter, members));
|
||||
ElasticCommonSchemaStructuredLogFormatter(Environment environment, ThrowableProxyConverter throwableProxyConverter,
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super((members) -> jsonMembers(environment, throwableProxyConverter, members), customizer);
|
||||
}
|
||||
|
||||
private static void jsonMembers(Environment environment, ThrowableProxyConverter throwableProxyConverter,
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.boot.json.WritableJson;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
@@ -69,8 +70,8 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
|
||||
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id");
|
||||
|
||||
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment,
|
||||
ThrowableProxyConverter throwableProxyConverter) {
|
||||
super((members) -> jsonMembers(environment, throwableProxyConverter, members));
|
||||
ThrowableProxyConverter throwableProxyConverter, StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super((members) -> jsonMembers(environment, throwableProxyConverter, members), customizer);
|
||||
}
|
||||
|
||||
private static void jsonMembers(Environment environment, ThrowableProxyConverter throwableProxyConverter,
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.json.JsonWriter.PairExtractor;
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
|
||||
/**
|
||||
@@ -48,8 +49,9 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<IL
|
||||
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
|
||||
(pair) -> pair.value);
|
||||
|
||||
LogstashStructuredLogFormatter(ThrowableProxyConverter throwableProxyConverter) {
|
||||
super((members) -> jsonMembers(throwableProxyConverter, members));
|
||||
LogstashStructuredLogFormatter(ThrowableProxyConverter throwableProxyConverter,
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super((members) -> jsonMembers(throwableProxyConverter, members), customizer);
|
||||
}
|
||||
|
||||
private static void jsonMembers(ThrowableProxyConverter throwableProxyConverter,
|
||||
|
||||
@@ -25,6 +25,7 @@ import ch.qos.logback.core.encoder.Encoder;
|
||||
import ch.qos.logback.core.encoder.EncoderBase;
|
||||
|
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
|
||||
@@ -85,24 +86,29 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
|
||||
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH, this::createLogstashFormatter);
|
||||
}
|
||||
|
||||
private StructuredLogFormatter<ILoggingEvent> createEcsFormatter(
|
||||
Instantiator<StructuredLogFormatter<ILoggingEvent>> instantiator) {
|
||||
private StructuredLogFormatter<ILoggingEvent> createEcsFormatter(Instantiator<?> instantiator) {
|
||||
Environment environment = instantiator.getArg(Environment.class);
|
||||
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
|
||||
return new ElasticCommonSchemaStructuredLogFormatter(environment, throwableProxyConverter);
|
||||
StructureLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
|
||||
.getArg(StructureLoggingJsonMembersCustomizer.class);
|
||||
return new ElasticCommonSchemaStructuredLogFormatter(environment, throwableProxyConverter,
|
||||
jsonMembersCustomizer);
|
||||
}
|
||||
|
||||
private StructuredLogFormatter<ILoggingEvent> createGraylogFormatter(
|
||||
Instantiator<StructuredLogFormatter<ILoggingEvent>> instantiator) {
|
||||
private StructuredLogFormatter<ILoggingEvent> createGraylogFormatter(Instantiator<?> instantiator) {
|
||||
Environment environment = instantiator.getArg(Environment.class);
|
||||
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
|
||||
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, throwableProxyConverter);
|
||||
StructureLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
|
||||
.getArg(StructureLoggingJsonMembersCustomizer.class);
|
||||
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, throwableProxyConverter,
|
||||
jsonMembersCustomizer);
|
||||
}
|
||||
|
||||
private StructuredLogFormatter<ILoggingEvent> createLogstashFormatter(
|
||||
Instantiator<StructuredLogFormatter<ILoggingEvent>> instantiator) {
|
||||
private StructuredLogFormatter<ILoggingEvent> createLogstashFormatter(Instantiator<?> instantiator) {
|
||||
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
|
||||
return new LogstashStructuredLogFormatter(throwableProxyConverter);
|
||||
StructureLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
|
||||
.getArg(StructureLoggingJsonMembersCustomizer.class);
|
||||
return new LogstashStructuredLogFormatter(throwableProxyConverter, jsonMembersCustomizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.json.JsonWriter.Members;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
|
||||
/**
|
||||
* Base class for {@link StructuredLogFormatter} implementations that generates JSON using
|
||||
@@ -38,9 +39,22 @@ public abstract class JsonWriterStructuredLogFormatter<E> implements StructuredL
|
||||
* Create a new {@link JsonWriterStructuredLogFormatter} instance with the given
|
||||
* members.
|
||||
* @param members a consumer, which should configure the members
|
||||
* @param customizer an optional customizer to apply
|
||||
*/
|
||||
protected JsonWriterStructuredLogFormatter(Consumer<Members<E>> members) {
|
||||
this(JsonWriter.of(members).withNewLineAtEnd());
|
||||
protected JsonWriterStructuredLogFormatter(Consumer<Members<E>> members,
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
this(JsonWriter.of(customized(members, customizer)).withNewLineAtEnd());
|
||||
}
|
||||
|
||||
private static <E> Consumer<Members<E>> customized(Consumer<Members<E>> members,
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
return (customizer != null) ? members.andThen(customizeWith(customizer)) : members;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <E> Consumer<Members<E>> customizeWith(StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
return (members) -> LambdaSafe.callback(StructureLoggingJsonMembersCustomizer.class, customizer, members)
|
||||
.invoke((instance) -> instance.customize(members));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.logging.structured;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.json.JsonWriter.Members;
|
||||
|
||||
/**
|
||||
* Customer that can be injected into a {@link StructuredLogFormatter} implementations to
|
||||
* customize {@link JsonWriter} {@link Members}.
|
||||
* <p>
|
||||
* An implementation may be provided using the {@code logging.structured.json.customizer}
|
||||
* property.
|
||||
*
|
||||
* @param <T> the type being written
|
||||
* @author Phillip Webb
|
||||
* @since 3.4.0
|
||||
* @see JsonWriterStructuredLogFormatter
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface StructureLoggingJsonMembersCustomizer<T> {
|
||||
|
||||
/**
|
||||
* Customize the given {@link Members} instance.
|
||||
* @param members the members instance to customize
|
||||
*/
|
||||
void customize(JsonWriter.Members<T> members);
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.springframework.core.env.Environment;
|
||||
* Implementing classes can declare the following parameter types in the constructor:
|
||||
* <ul>
|
||||
* <li>{@link Environment}</li>
|
||||
* <li>{@link StructureLoggingJsonMembersCustomizer}</li>
|
||||
* </ul>
|
||||
* When using Logback, implementing classes can also use the following parameter types in
|
||||
* the constructor:
|
||||
|
||||
@@ -16,16 +16,22 @@
|
||||
|
||||
package org.springframework.boot.logging.structured;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter.Members;
|
||||
import org.springframework.boot.util.Instantiator;
|
||||
import org.springframework.boot.util.Instantiator.AvailableParameters;
|
||||
import org.springframework.boot.util.Instantiator.FailureHandler;
|
||||
import org.springframework.boot.util.LambdaSafe;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -48,9 +54,11 @@ public class StructuredLogFormatterFactory<E> {
|
||||
}
|
||||
};
|
||||
|
||||
private final SpringFactoriesLoader factoriesLoader;
|
||||
|
||||
private final Class<E> logEventType;
|
||||
|
||||
private final Instantiator<StructuredLogFormatter<E>> instantiator;
|
||||
private final Instantiator<?> instantiator;
|
||||
|
||||
private final CommonFormatters<E> commonFormatters;
|
||||
|
||||
@@ -64,9 +72,18 @@ public class StructuredLogFormatterFactory<E> {
|
||||
*/
|
||||
public StructuredLogFormatterFactory(Class<E> logEventType, Environment environment,
|
||||
Consumer<AvailableParameters> availableParameters, Consumer<CommonFormatters<E>> commonFormatters) {
|
||||
this(SpringFactoriesLoader.forDefaultResourceLocation(), logEventType, environment, availableParameters,
|
||||
commonFormatters);
|
||||
}
|
||||
|
||||
StructuredLogFormatterFactory(SpringFactoriesLoader factoriesLoader, Class<E> logEventType, Environment environment,
|
||||
Consumer<AvailableParameters> availableParameters, Consumer<CommonFormatters<E>> commonFormatters) {
|
||||
this.factoriesLoader = factoriesLoader;
|
||||
this.logEventType = logEventType;
|
||||
this.instantiator = new Instantiator<>(StructuredLogFormatter.class, (allAvailableParameters) -> {
|
||||
this.instantiator = new Instantiator<>(Object.class, (allAvailableParameters) -> {
|
||||
allAvailableParameters.add(Environment.class, environment);
|
||||
allAvailableParameters.add(StructureLoggingJsonMembersCustomizer.class,
|
||||
(type) -> getStructureLoggingJsonMembersCustomizer(environment));
|
||||
if (availableParameters != null) {
|
||||
availableParameters.accept(allAvailableParameters);
|
||||
}
|
||||
@@ -75,6 +92,29 @@ public class StructuredLogFormatterFactory<E> {
|
||||
commonFormatters.accept(this.commonFormatters);
|
||||
}
|
||||
|
||||
StructureLoggingJsonMembersCustomizer<?> getStructureLoggingJsonMembersCustomizer(Environment environment) {
|
||||
List<StructureLoggingJsonMembersCustomizer<?>> customizers = new ArrayList<>();
|
||||
StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment);
|
||||
if (properties != null) {
|
||||
customizers.add(new StructuredLoggingJsonPropertiesJsonMembersCustomizer(this.instantiator, properties));
|
||||
}
|
||||
customizers.addAll(loadStructureLoggingJsonMembersCustomizers());
|
||||
return (members) -> invokeCustomizers(customizers, members);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private List<StructureLoggingJsonMembersCustomizer<?>> loadStructureLoggingJsonMembersCustomizers() {
|
||||
return (List) this.factoriesLoader.load(StructureLoggingJsonMembersCustomizer.class,
|
||||
ArgumentResolver.from(this.instantiator::getArg));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void invokeCustomizers(List<StructureLoggingJsonMembersCustomizer<?>> customizers,
|
||||
Members<Object> members) {
|
||||
LambdaSafe.callbacks(StructureLoggingJsonMembersCustomizer.class, customizers, members)
|
||||
.invoke((customizer) -> customizer.customize(members));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new {@link StructuredLogFormatter} instance for the specified format.
|
||||
* @param format the format requested (either a {@link CommonStructuredLogFormat} ID
|
||||
@@ -93,12 +133,15 @@ public class StructuredLogFormatterFactory<E> {
|
||||
.formatted(format, this.commonFormatters.getCommonNames()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private StructuredLogFormatter<E> getUsingClassName(String className) {
|
||||
StructuredLogFormatter<E> formatter = this.instantiator.instantiate(className);
|
||||
Object formatter = this.instantiator.instantiate(className);
|
||||
if (formatter != null) {
|
||||
Assert.state(formatter instanceof StructuredLogFormatter,
|
||||
() -> "'%s' is not a StructuredLogFormatter".formatted(className));
|
||||
checkTypeArgument(formatter);
|
||||
}
|
||||
return formatter;
|
||||
return (StructuredLogFormatter<E>) formatter;
|
||||
}
|
||||
|
||||
private void checkTypeArgument(Object formatter) {
|
||||
@@ -134,7 +177,7 @@ public class StructuredLogFormatterFactory<E> {
|
||||
return this.factories.keySet().stream().map(CommonStructuredLogFormat::getId).toList();
|
||||
}
|
||||
|
||||
StructuredLogFormatter<E> get(Instantiator<StructuredLogFormatter<E>> instantiator, String format) {
|
||||
StructuredLogFormatter<E> get(Instantiator<?> 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;
|
||||
@@ -156,7 +199,7 @@ public class StructuredLogFormatterFactory<E> {
|
||||
* @param instantiator instantiator that can be used to obtain arguments
|
||||
* @return a new {@link StructuredLogFormatter} instance
|
||||
*/
|
||||
StructuredLogFormatter<E> createFormatter(Instantiator<StructuredLogFormatter<E>> instantiator);
|
||||
StructuredLogFormatter<E> createFormatter(Instantiator<?> instantiator);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Properties that can be used to customize structured logging JSON.
|
||||
*
|
||||
* @param include the paths that should be included. An empty set includes all names
|
||||
* @param exclude the paths that should be excluded. An empty set excludes nothing
|
||||
* @param rename a map of path to replacement names
|
||||
* @param add a map of additional elements {@link StructureLoggingJsonMembersCustomizer}
|
||||
* @param customizer the fully qualified name of a
|
||||
* {@link StructureLoggingJsonMembersCustomizer}
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
record StructuredLoggingJsonProperties(Set<String> include, Set<String> exclude, Map<String, String> rename,
|
||||
Map<String, String> add, String customizer) {
|
||||
|
||||
static StructuredLoggingJsonProperties get(Environment environment) {
|
||||
return Binder.get(environment)
|
||||
.bind("logging.structured.json", StructuredLoggingJsonProperties.class)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.Map;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter.MemberPath;
|
||||
import org.springframework.boot.json.JsonWriter.Members;
|
||||
import org.springframework.boot.util.Instantiator;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link StructureLoggingJsonMembersCustomizer} to apply
|
||||
* {@link StructuredLoggingJsonProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class StructuredLoggingJsonPropertiesJsonMembersCustomizer implements StructureLoggingJsonMembersCustomizer<Object> {
|
||||
|
||||
private final Instantiator<?> instantiator;
|
||||
|
||||
private final StructuredLoggingJsonProperties properties;
|
||||
|
||||
StructuredLoggingJsonPropertiesJsonMembersCustomizer(Instantiator<?> instantiator,
|
||||
StructuredLoggingJsonProperties properties) {
|
||||
this.instantiator = instantiator;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Members<Object> members) {
|
||||
members.applyingPathFilter(this::filterPath);
|
||||
members.applyingNameProcessor(this::renameJsonMembers);
|
||||
Map<String, String> add = this.properties.add();
|
||||
if (!CollectionUtils.isEmpty(add)) {
|
||||
add.forEach(members::add);
|
||||
}
|
||||
String customizer = this.properties.customizer();
|
||||
if (StringUtils.hasLength(customizer)) {
|
||||
createAndApplyCustomizer(members, customizer);
|
||||
}
|
||||
}
|
||||
|
||||
String renameJsonMembers(MemberPath path, String existingName) {
|
||||
Map<String, String> rename = this.properties.rename();
|
||||
String key = path.toUnescapedString();
|
||||
return !CollectionUtils.isEmpty(rename) ? rename.getOrDefault(key, existingName) : existingName;
|
||||
}
|
||||
|
||||
boolean filterPath(MemberPath path) {
|
||||
boolean included = CollectionUtils.isEmpty(this.properties.include())
|
||||
|| this.properties.include().contains(path.toUnescapedString());
|
||||
boolean excluded = !CollectionUtils.isEmpty(this.properties.exclude())
|
||||
&& this.properties.exclude().contains(path.toUnescapedString());
|
||||
return (!included || excluded);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private void createAndApplyCustomizer(Members<Object> members, String customizerClassName) {
|
||||
((StructureLoggingJsonMembersCustomizer) this.instantiator.instantiate(customizerClassName)).customize(members);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -265,9 +265,29 @@
|
||||
"description": "Structured GELF service version (defaults to 'spring.application.version')."
|
||||
},
|
||||
{
|
||||
"name": "logging.structured.gelf.service.version",
|
||||
"name": "logging.structured.json.add",
|
||||
"type": "java.util.Map<java.lang.String,java.lang.String>",
|
||||
"description": "Additional members that should be added to structured logging JSON"
|
||||
},
|
||||
{
|
||||
"name": "logging.structured.json.customizer",
|
||||
"type": "java.lang.String",
|
||||
"description": "Structured GELF service version (defaults to 'spring.application.version')."
|
||||
"description": "The fully qualified class name of a StructureLoggingJsonMembersCustomizer"
|
||||
},
|
||||
{
|
||||
"name": "logging.structured.json.exclude",
|
||||
"type": "java.util.Set<java.lang.String>",
|
||||
"description": "Member paths that should be excluded from structured logging JSON"
|
||||
},
|
||||
{
|
||||
"name": "logging.structured.json.include",
|
||||
"type": "java.util.Set<java.lang.String>",
|
||||
"description": "Member paths that should be included in structured logging JSON"
|
||||
},
|
||||
{
|
||||
"name": "logging.structured.json.rename",
|
||||
"type": "java.util.Map<java.lang.String,java.lang.String>",
|
||||
"description": "Mapping between member paths and an alternative name that should be used in structured logging JSON"
|
||||
},
|
||||
{
|
||||
"name": "logging.threshold.console",
|
||||
|
||||
@@ -27,6 +27,11 @@ 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 org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -35,12 +40,16 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
abstract class AbstractStructuredLoggingTests {
|
||||
|
||||
static final Instant EVENT_TIME = Instant.ofEpochMilli(1719910193000L);
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
@Mock
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer;
|
||||
|
||||
protected Map<String, Object> map(Object... values) {
|
||||
assertThat(values.length).isEven();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
@@ -27,6 +27,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link ElasticCommonSchemaStructuredLogFormatter}.
|
||||
@@ -45,7 +47,12 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
||||
environment.setProperty("logging.structured.ecs.service.environment", "test");
|
||||
environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
|
||||
environment.setProperty("spring.application.pid", "1");
|
||||
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(environment);
|
||||
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(environment, this.customizer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsCustomizer() {
|
||||
then(this.customizer).should().customize(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -29,6 +29,8 @@ import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link GraylogExtendedLogFormatStructuredLogFormatter}.
|
||||
@@ -47,7 +49,12 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
|
||||
environment.setProperty("logging.structured.gelf.host", "name");
|
||||
environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
|
||||
environment.setProperty("spring.application.pid", "1");
|
||||
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment);
|
||||
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment, this.customizer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsCustomizer() {
|
||||
then(this.customizer).should().customize(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,6 +30,8 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link LogstashStructuredLogFormatter}.
|
||||
@@ -42,7 +44,12 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.formatter = new LogstashStructuredLogFormatter();
|
||||
this.formatter = new LogstashStructuredLogFormatter(this.customizer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsCustomizer() {
|
||||
then(this.customizer).should().customize(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -31,10 +31,15 @@ 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.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.event.KeyValuePair;
|
||||
import org.slf4j.helpers.BasicMarkerFactory;
|
||||
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
@@ -42,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
abstract class AbstractStructuredLoggingTests {
|
||||
|
||||
static final Instant EVENT_TIME = Instant.ofEpochSecond(1719910193L);
|
||||
@@ -52,6 +58,9 @@ abstract class AbstractStructuredLoggingTests {
|
||||
|
||||
private BasicMarkerFactory markerFactory;
|
||||
|
||||
@Mock
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.markerFactory = new BasicMarkerFactory();
|
||||
|
||||
@@ -27,6 +27,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link ElasticCommonSchemaStructuredLogFormatter}.
|
||||
@@ -47,7 +49,13 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
|
||||
environment.setProperty("logging.structured.ecs.service.environment", "test");
|
||||
environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
|
||||
environment.setProperty("spring.application.pid", "1");
|
||||
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(environment, getThrowableProxyConverter());
|
||||
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(environment, getThrowableProxyConverter(),
|
||||
this.customizer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsCustomizer() {
|
||||
then(this.customizer).should().customize(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,6 +30,8 @@ import org.springframework.boot.testsupport.system.OutputCaptureExtension;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link GraylogExtendedLogFormatStructuredLogFormatter}.
|
||||
@@ -50,7 +52,13 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
|
||||
environment.setProperty("logging.structured.gelf.host", "name");
|
||||
environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
|
||||
environment.setProperty("spring.application.pid", "1");
|
||||
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment, getThrowableProxyConverter());
|
||||
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment, getThrowableProxyConverter(),
|
||||
this.customizer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsCustomizer() {
|
||||
then(this.customizer).should().customize(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,6 +30,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Marker;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
|
||||
/**
|
||||
* Tests for {@link LogstashStructuredLogFormatter}.
|
||||
@@ -44,7 +46,12 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
super.setUp();
|
||||
this.formatter = new LogstashStructuredLogFormatter(getThrowableProxyConverter());
|
||||
this.formatter = new LogstashStructuredLogFormatter(getThrowableProxyConverter(), this.customizer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void callsCustomizer() {
|
||||
then(this.customizer).should().customize(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -16,15 +16,23 @@
|
||||
|
||||
package org.springframework.boot.logging.structured;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter.ValueProcessor;
|
||||
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
|
||||
import org.springframework.boot.util.Instantiator.AvailableParameters;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link StructuredLogFormatterFactory}.
|
||||
@@ -95,6 +103,19 @@ class StructuredLogFormatterFactoryTests {
|
||||
assertThat(formatter.getCustom()).hasToString("Hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInjectCustomizers() {
|
||||
this.environment.setProperty("logging.structured.json.rename.spring", "test");
|
||||
SpringFactoriesLoader factoriesLoader = mock(SpringFactoriesLoader.class);
|
||||
StructureLoggingJsonMembersCustomizer<?> customizer = (members) -> members
|
||||
.applyingValueProcessor(ValueProcessor.of(String.class, String::toUpperCase));
|
||||
given(factoriesLoader.load(any(), any(ArgumentResolver.class))).willReturn(List.of(customizer));
|
||||
StructuredLogFormatterFactory<LogEvent> factory = new StructuredLogFormatterFactory<>(factoriesLoader,
|
||||
LogEvent.class, this.environment, this::addAvailableParameters, this::addCommonFormatters);
|
||||
CutomizedFormatter formatter = (CutomizedFormatter) factory.get(CutomizedFormatter.class.getName());
|
||||
assertThat(formatter.format(new LogEvent())).contains("\"test\":\"BOOT\"");
|
||||
}
|
||||
|
||||
static class LogEvent {
|
||||
|
||||
}
|
||||
@@ -146,4 +167,12 @@ class StructuredLogFormatterFactoryTests {
|
||||
|
||||
}
|
||||
|
||||
static class CutomizedFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
|
||||
|
||||
CutomizedFormatter(StructureLoggingJsonMembersCustomizer<?> customizer) {
|
||||
super((members) -> members.add("spring", "boot"), customizer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.json.JsonWriter;
|
||||
import org.springframework.boot.json.JsonWriter.NameProcessor;
|
||||
import org.springframework.boot.util.Instantiator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
|
||||
/**
|
||||
* Tests for {@link StructuredLoggingJsonPropertiesJsonMembersCustomizer}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
|
||||
|
||||
@Mock
|
||||
private Instantiator<?> instantiator;
|
||||
|
||||
@Test
|
||||
void customizeWhenHasExcludeFiltersMember() {
|
||||
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
|
||||
Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null);
|
||||
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
|
||||
this.instantiator, properties);
|
||||
assertThat(writeSampleJson(customizer)).doesNotContain("a").contains("b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenHasIncludeFiltersOtherMembers() {
|
||||
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a"),
|
||||
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null);
|
||||
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
|
||||
this.instantiator, properties);
|
||||
assertThat(writeSampleJson(customizer)).contains("a")
|
||||
.doesNotContain("b")
|
||||
.doesNotContain("c")
|
||||
.doesNotContain("d");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenHasIncludeAndExcludeFiltersMembers() {
|
||||
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("b"),
|
||||
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
|
||||
this.instantiator, properties);
|
||||
assertThat(writeSampleJson(customizer)).contains("a")
|
||||
.doesNotContain("b")
|
||||
.doesNotContain("c")
|
||||
.doesNotContain("d");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenHasRenameRenamesMember() {
|
||||
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
|
||||
Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null);
|
||||
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
|
||||
this.instantiator, properties);
|
||||
assertThat(writeSampleJson(customizer)).contains("\"z\":\"a\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeWhenHasAddAddsMemeber() {
|
||||
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
|
||||
Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null);
|
||||
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
|
||||
this.instantiator, properties);
|
||||
assertThat(writeSampleJson(customizer)).contains("\"z\":\"z\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void customizeWhenHasCustomizerCustomizesMember() {
|
||||
StructureLoggingJsonMembersCustomizer<?> uppercaseCustomizer = (members) -> members
|
||||
.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||
given(((Instantiator) this.instantiator).instantiate("test")).willReturn(uppercaseCustomizer);
|
||||
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
|
||||
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), "test");
|
||||
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
|
||||
this.instantiator, properties);
|
||||
assertThat(writeSampleJson(customizer)).contains("\"A\":\"a\"");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private String writeSampleJson(StructureLoggingJsonMembersCustomizer customizer) {
|
||||
return JsonWriter.of((members) -> {
|
||||
members.add("a", "a");
|
||||
members.add("b", "b");
|
||||
members.add("c", "c");
|
||||
customizer.customize(members);
|
||||
}).writeToString(new Object());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link StructuredLoggingJsonProperties}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class StructuredLoggingJsonPropertiesTests {
|
||||
|
||||
@Test
|
||||
void getBindsFromEnvironment() {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
environment.setProperty("logging.structured.json.include", "a,b");
|
||||
environment.setProperty("logging.structured.json.exclude", "c,d");
|
||||
environment.setProperty("logging.structured.json.rename.e", "f");
|
||||
environment.setProperty("logging.structured.json.add.g", "h");
|
||||
environment.setProperty("logging.structured.json.customizer", "i");
|
||||
StructuredLoggingJsonProperties properties = StructuredLoggingJsonProperties.get(environment);
|
||||
assertThat(properties).isEqualTo(new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("c", "d"),
|
||||
Map.of("e", "f"), Map.of("g", "h"), "i"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWhenNoBoundPropertiesReturnsNull() {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
StructuredLoggingJsonProperties.get(environment);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.json.JsonWriter.Members;
|
||||
import org.springframework.boot.json.JsonWriter.ValueProcessor;
|
||||
import org.springframework.boot.logging.structured.StructureLoggingJsonMembersCustomizer;
|
||||
|
||||
public class SampleJsonMembersCustomizer implements StructureLoggingJsonMembersCustomizer<Object> {
|
||||
|
||||
@Override
|
||||
public void customize(Members<Object> members) {
|
||||
members.applyingValueProcessor(
|
||||
ValueProcessor.of(String.class, "!!%s!!"::formatted).whenHasUnescapedPath("process.thread.name"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
bar=hello
|
||||
logging.structured.format.console=ecs
|
||||
logging.structured.json.exclude=@timestamp
|
||||
logging.structured.json.rename[process.pid]=process.procid
|
||||
logging.structured.json.add.foo=${bar}
|
||||
logging.structured.json.customizer=smoketest.structuredlogging.SampleJsonMembersCustomizer
|
||||
#---
|
||||
spring.config.activate.on-profile=custom
|
||||
logging.structured.format.console=smoketest.structuredlogging.CustomStructuredLogFormatter
|
||||
|
||||
@@ -52,8 +52,11 @@ class SampleStructuredLoggingApplicationTests {
|
||||
@Test
|
||||
void json(CapturedOutput output) {
|
||||
SampleStructuredLoggingApplication.main(new String[0]);
|
||||
assertThat(output).contains("{\"@timestamp\"")
|
||||
.contains("\"message\":\"Starting SampleStructuredLoggingApplication");
|
||||
assertThat(output).doesNotContain("{\"@timestamp\"")
|
||||
.contains("\"process.thread.name\":\"!!")
|
||||
.contains("\"process.procid\"")
|
||||
.contains("\"message\":\"Starting SampleStructuredLoggingApplication")
|
||||
.contains("\"foo\":\"hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user