Allow structured logging with relocated or disabled context elements

Update `StructuredLoggingJsonProperties` to support context properties
that allows MDC data to not be logged, or to be logged in a different
location.

Closes gh-45218
This commit is contained in:
Phillip Webb 2025-04-16 19:20:32 -07:00
parent fdd4abc895
commit 0c32011626
25 changed files with 611 additions and 260 deletions

View File

@ -16,7 +16,6 @@
package org.springframework.boot.logging.log4j2;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
@ -31,7 +30,8 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaPairs;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.ContextPairs.Pairs;
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
@ -49,12 +49,12 @@ import org.springframework.util.ObjectUtils;
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, members), customizer);
ContextPairs contextPairs, StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, members), customizer);
}
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
JsonWriter.Members<LogEvent> members) {
ContextPairs contextPairs, JsonWriter.Members<LogEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter);
members.add("@timestamp", LogEvent::getInstant).as(ElasticCommonSchemaStructuredLogFormatter::asTimestamp);
members.add("log").usingMembers((log) -> {
@ -68,9 +68,7 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
ElasticCommonSchemaProperties.get(environment).jsonMembers(members);
members.add("message", LogEvent::getMessage).as(StructuredMessage::get);
members.from(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.as((contextData) -> ElasticCommonSchemaPairs.nested((nested) -> contextData.forEach(nested::accept)))
.usingPairs(Map::forEach);
.usingPairs(contextPairs.nested(ElasticCommonSchemaStructuredLogFormatter::addContextDataPairs));
members.from(LogEvent::getThrownProxy).whenNotNull().usingMembers((thrownProxyMembers) -> {
thrownProxyMembers.add("error").usingMembers((error) -> {
error.add("type", ThrowableProxy::getThrowable).whenNotNull().as(ObjectUtils::nullSafeClassName);
@ -85,6 +83,10 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
members.add("ecs").usingMembers((ecs) -> ecs.add("version", "8.11"));
}
private static void addContextDataPairs(Pairs<ReadOnlyStringMap> contextPairs) {
contextPairs.add((contextData, pairs) -> contextData.forEach(pairs::accept));
}
private static java.time.Instant asTimestamp(Instant instant) {
return java.time.Instant.ofEpochMilli(instant.getEpochMillisecond()).plusNanos(instant.getNanoOfMillisecond());
}

View File

@ -19,7 +19,7 @@ package org.springframework.boot.logging.log4j2;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
@ -36,13 +36,13 @@ import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.WritableJson;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;
import org.springframework.core.env.Environment;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@ -72,12 +72,12 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id");
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, members), customizer);
ContextPairs contextPairs, StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, members), customizer);
}
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
JsonWriter.Members<LogEvent> members) {
ContextPairs contextPairs, JsonWriter.Members<LogEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter);
members.add("version", "1.1");
members.add("short_message", LogEvent::getMessage)
@ -93,7 +93,8 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
members.add("_log_logger", LogEvent::getLoggerName);
members.from(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.usingPairs(GraylogExtendedLogFormatStructuredLogFormatter::createAdditionalFields);
.usingPairs(contextPairs.flat(additionalFieldJoiner(),
GraylogExtendedLogFormatStructuredLogFormatter::addContextDataPairs));
members.add()
.whenNotNull(LogEvent::getThrownProxy)
.usingMembers((thrownProxyMembers) -> throwableMembers(thrownProxyMembers, extractor));
@ -135,25 +136,23 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
members.add("_error_message", (event) -> event.getThrownProxy().getMessage());
}
private static void createAdditionalFields(ReadOnlyStringMap contextData, BiConsumer<Object, Object> pairs) {
contextData.forEach((name, value) -> createAdditionalField(name, value, pairs));
private static void addContextDataPairs(ContextPairs.Pairs<ReadOnlyStringMap> contextPairs) {
contextPairs.add((contextData, pairs) -> contextData.forEach(pairs::accept));
}
private static void createAdditionalField(String name, Object value, BiConsumer<Object, Object> pairs) {
Assert.notNull(name, "'name' must not be null");
if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", name));
return;
}
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(name)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", name));
return;
}
pairs.accept(asAdditionalFieldName(name), value);
}
private static Object asAdditionalFieldName(String name) {
return (!name.startsWith("_")) ? "_" + name : name;
private static BinaryOperator<String> additionalFieldJoiner() {
return (prefix, name) -> {
name = prefix + name;
if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", name));
return null;
}
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(name)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", name));
return null;
}
return (!name.startsWith("_")) ? "_" + name : name;
};
}
}

View File

@ -31,6 +31,7 @@ import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;
@ -44,12 +45,13 @@ import org.springframework.util.CollectionUtils;
*/
class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<LogEvent> {
LogstashStructuredLogFormatter(StackTracePrinter stackTracePrinter,
LogstashStructuredLogFormatter(StackTracePrinter stackTracePrinter, ContextPairs contextPairs,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(stackTracePrinter, members), customizer);
super((members) -> jsonMembers(stackTracePrinter, contextPairs, members), customizer);
}
private static void jsonMembers(StackTracePrinter stackTracePrinter, JsonWriter.Members<LogEvent> members) {
private static void jsonMembers(StackTracePrinter stackTracePrinter, ContextPairs contextPairs,
JsonWriter.Members<LogEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter);
members.add("@timestamp", LogEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
members.add("@version", "1");
@ -60,7 +62,7 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<Lo
members.add("level_value", LogEvent::getLevel).as(Level::intLevel);
members.from(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.usingPairs((contextData, pairs) -> contextData.forEach(pairs::accept));
.usingPairs(contextPairs.flat("_", LogstashStructuredLogFormatter::addContextDataPairs));
members.add("tags", LogEvent::getMarker)
.whenNotNull()
.as(LogstashStructuredLogFormatter::getMarkers)
@ -75,6 +77,10 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<Lo
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime);
}
private static void addContextDataPairs(ContextPairs.Pairs<ReadOnlyStringMap> contextPairs) {
contextPairs.add((contextData, pairs) -> contextData.forEach(pairs::accept));
}
private static Set<String> getMarkers(Marker marker) {
Set<String> result = new TreeSet<>();
addMarkers(result, marker);

View File

@ -31,6 +31,7 @@ import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
@ -113,25 +114,29 @@ final class StructuredLogLayout extends AbstractStringLayout {
private ElasticCommonSchemaStructuredLogFormatter createEcsFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ContextPairs contextPairs = instantiator.getArg(ContextPairs.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, jsonMembersCustomizer);
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, contextPairs,
jsonMembersCustomizer);
}
private GraylogExtendedLogFormatStructuredLogFormatter createGraylogFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ContextPairs contextPairs = instantiator.getArg(ContextPairs.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, stackTracePrinter,
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, stackTracePrinter, contextPairs,
jsonMembersCustomizer);
}
private LogstashStructuredLogFormatter createLogstashFormatter(Instantiator<?> instantiator) {
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ContextPairs contextPairs = instantiator.getArg(ContextPairs.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new LogstashStructuredLogFormatter(stackTracePrinter, jsonMembersCustomizer);
return new LogstashStructuredLogFormatter(stackTracePrinter, contextPairs, jsonMembersCustomizer);
}
}

View File

@ -18,7 +18,6 @@ package org.springframework.boot.logging.logback;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
@ -30,9 +29,10 @@ 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.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ElasticCommonSchemaPairs;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.ElasticCommonSchemaProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
@ -48,13 +48,19 @@ import org.springframework.core.env.Environment;
*/
class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
ElasticCommonSchemaStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, throwableProxyConverter, members), customizer);
ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, throwableProxyConverter, members),
customizer);
}
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter);
members.add("@timestamp", ILoggingEvent::getInstant);
members.add("log").usingMembers((log) -> {
@ -67,14 +73,10 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
});
ElasticCommonSchemaProperties.get(environment).jsonMembers(members);
members.add("message", ILoggingEvent::getFormattedMessage);
members.from(ILoggingEvent::getMDCPropertyMap)
.whenNotEmpty()
.as(ElasticCommonSchemaPairs::nested)
.usingPairs(Map::forEach);
members.from(ILoggingEvent::getKeyValuePairs)
.whenNotEmpty()
.as(ElasticCommonSchemaStructuredLogFormatter::nested)
.usingPairs(Map::forEach);
members.add().usingPairs(contextPairs.nested((pairs) -> {
pairs.addMapEntries(ILoggingEvent::getMDCPropertyMap);
pairs.add(ILoggingEvent::getKeyValuePairs, keyValuePairExtractor);
}));
members.add().whenNotNull(ILoggingEvent::getThrowableProxy).usingMembers((throwableMembers) -> {
throwableMembers.add("error").usingMembers((error) -> {
error.add("type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
@ -89,11 +91,6 @@ class ElasticCommonSchemaStructuredLogFormatter extends JsonWriterStructuredLogF
members.add("ecs").usingMembers((ecs) -> ecs.add("version", "8.11"));
}
private static Map<String, Object> nested(List<KeyValuePair> keyValuePairs) {
return ElasticCommonSchemaPairs.nested((nested) -> keyValuePairs
.forEach((keyValuePair) -> nested.accept(keyValuePair.key, keyValuePair.value)));
}
private static Set<String> getMarkers(List<Marker> markers) {
Set<String> result = new TreeSet<>();
addMarkers(result, markers.iterator());

View File

@ -17,10 +17,9 @@
package org.springframework.boot.logging.logback;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.regex.Pattern;
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
@ -33,17 +32,17 @@ import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.boot.json.WritableJson;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatProperties;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;
import org.springframework.core.env.Environment;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@ -57,6 +56,9 @@ import org.springframework.util.StringUtils;
*/
class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> {
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
private static final Log logger = LogFactory.getLog(GraylogExtendedLogFormatStructuredLogFormatter.class);
/**
@ -72,12 +74,15 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id");
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, throwableProxyConverter, members), customizer);
ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(environment, stackTracePrinter, contextPairs, throwableProxyConverter, members),
customizer);
}
private static void jsonMembers(Environment environment, StackTracePrinter stackTracePrinter,
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter);
members.add("version", "1.1");
members.add("short_message", ILoggingEvent::getFormattedMessage)
@ -91,12 +96,10 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
members.add("_process_thread_name", ILoggingEvent::getThreadName);
GraylogExtendedLogFormatProperties.get(environment).jsonMembers(members);
members.add("_log_logger", ILoggingEvent::getLoggerName);
members.from(ILoggingEvent::getMDCPropertyMap)
.when((mdc) -> !CollectionUtils.isEmpty(mdc))
.usingPairs((mdc, pairs) -> mdc.forEach((key, value) -> createAdditionalField(key, value, pairs)));
members.from(ILoggingEvent::getKeyValuePairs)
.when((keyValuePairs) -> !CollectionUtils.isEmpty(keyValuePairs))
.usingPairs(GraylogExtendedLogFormatStructuredLogFormatter::createAdditionalField);
members.add().usingPairs(contextPairs.flat(additionalFieldJoiner(), (pairs) -> {
pairs.addMapEntries(ILoggingEvent::getMDCPropertyMap);
pairs.add(ILoggingEvent::getKeyValuePairs, keyValuePairExtractor);
}));
members.add()
.whenNotNull(ILoggingEvent::getThrowableProxy)
.usingMembers((throwableMembers) -> throwableMembers(throwableMembers, extractor));
@ -125,25 +128,19 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
members.add("_error_message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
}
private static void createAdditionalField(List<KeyValuePair> keyValuePairs, BiConsumer<Object, Object> pairs) {
keyValuePairs.forEach((keyValuePair) -> createAdditionalField(keyValuePair.key, keyValuePair.value, pairs));
}
private static void createAdditionalField(String name, Object value, BiConsumer<Object, Object> pairs) {
Assert.notNull(name, "'name' must not be null");
if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", name));
return;
}
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(name)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", name));
return;
}
pairs.accept(asAdditionalFieldName(name), value);
}
private static Object asAdditionalFieldName(String name) {
return (!name.startsWith("_")) ? "_" + name : name;
private static BinaryOperator<String> additionalFieldJoiner() {
return (prefix, name) -> {
name = prefix + name;
if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", name));
return null;
}
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(name)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", name));
return null;
}
return (!name.startsWith("_")) ? "_" + name : name;
};
}
}

View File

@ -35,6 +35,7 @@ import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;
@ -50,12 +51,12 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<IL
private static final PairExtractor<KeyValuePair> keyValuePairExtractor = PairExtractor.of((pair) -> pair.key,
(pair) -> pair.value);
LogstashStructuredLogFormatter(StackTracePrinter stackTracePrinter, ThrowableProxyConverter throwableProxyConverter,
StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(stackTracePrinter, throwableProxyConverter, members), customizer);
LogstashStructuredLogFormatter(StackTracePrinter stackTracePrinter, ContextPairs contextPairs,
ThrowableProxyConverter throwableProxyConverter, StructuredLoggingJsonMembersCustomizer<?> customizer) {
super((members) -> jsonMembers(stackTracePrinter, contextPairs, throwableProxyConverter, members), customizer);
}
private static void jsonMembers(StackTracePrinter stackTracePrinter,
private static void jsonMembers(StackTracePrinter stackTracePrinter, ContextPairs contextPairs,
ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) {
Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter);
members.add("@timestamp", ILoggingEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp);
@ -65,10 +66,10 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<IL
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.from(ILoggingEvent::getKeyValuePairs)
.whenNotEmpty()
.usingExtractedPairs(Iterable::forEach, keyValuePairExtractor);
members.add().usingPairs(contextPairs.flat("_", (pairs) -> {
pairs.addMapEntries(ILoggingEvent::getMDCPropertyMap);
pairs.add(ILoggingEvent::getKeyValuePairs, keyValuePairExtractor);
}));
members.add("tags", ILoggingEvent::getMarkerList)
.whenNotNull()
.as(LogstashStructuredLogFormatter::getMarkers)

View File

@ -26,6 +26,7 @@ import ch.qos.logback.core.encoder.EncoderBase;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.ContextPairs;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
@ -90,29 +91,33 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
private StructuredLogFormatter<ILoggingEvent> createEcsFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ContextPairs contextParis = instantiator.getArg(ContextPairs.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, throwableProxyConverter,
jsonMembersCustomizer);
return new ElasticCommonSchemaStructuredLogFormatter(environment, stackTracePrinter, contextParis,
throwableProxyConverter, jsonMembersCustomizer);
}
private StructuredLogFormatter<ILoggingEvent> createGraylogFormatter(Instantiator<?> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ContextPairs contextParis = instantiator.getArg(ContextPairs.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, stackTracePrinter,
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, stackTracePrinter, contextParis,
throwableProxyConverter, jsonMembersCustomizer);
}
private StructuredLogFormatter<ILoggingEvent> createLogstashFormatter(Instantiator<?> instantiator) {
StackTracePrinter stackTracePrinter = instantiator.getArg(StackTracePrinter.class);
ContextPairs contextParis = instantiator.getArg(ContextPairs.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
StructuredLoggingJsonMembersCustomizer<?> jsonMembersCustomizer = instantiator
.getArg(StructuredLoggingJsonMembersCustomizer.class);
return new LogstashStructuredLogFormatter(stackTracePrinter, throwableProxyConverter, jsonMembersCustomizer);
return new LogstashStructuredLogFormatter(stackTracePrinter, contextParis, throwableProxyConverter,
jsonMembersCustomizer);
}
@Override

View File

@ -0,0 +1,210 @@
/*
* Copyright 2012-2025 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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.PairExtractor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Helper that can be used to add JSON pairs from context data (typically the logger MDC)
* in the correct location (or drop them altogether).
*
* @author Phillip Webb
* @since 3.5.0
*/
public class ContextPairs {
private final boolean include;
private final String prefix;
ContextPairs(boolean include, String prefix) {
this.include = include;
this.prefix = (prefix != null) ? prefix : "";
}
/**
* Add pairs using flat naming.
* @param <T> the item type
* @param delimeter the delimiter used if there is a prefix
* @param pairs callback to add all the pairs
* @return a {@link BiConsumer} for use with the {@link JsonWriter}
*/
public <T> BiConsumer<T, BiConsumer<String, Object>> flat(String delimeter, Consumer<Pairs<T>> pairs) {
return flat(joinWith(delimeter), pairs);
}
/**
* Add pairs using flat naming.
* @param <T> the item type
* @param joiner the function used to join the prefix and name
* @param pairs callback to add all the pairs
* @return a {@link BiConsumer} for use with the {@link JsonWriter}
*/
public <T> BiConsumer<T, BiConsumer<String, Object>> flat(BinaryOperator<String> joiner, Consumer<Pairs<T>> pairs) {
return (!this.include) ? none() : new Pairs<>(joiner, pairs)::flat;
}
/**
* Add pairs using nested naming (for example as used in ECS).
* @param <T> the item type
* @param pairs callback to add all the pairs
* @return a {@link BiConsumer} for use with the {@link JsonWriter}
*/
public <T> BiConsumer<T, BiConsumer<String, Object>> nested(Consumer<Pairs<T>> pairs) {
return (!this.include) ? none() : new Pairs<>(joinWith("."), pairs)::nested;
}
private <T, V> BiConsumer<T, BiConsumer<String, V>> none() {
return (item, pairs) -> {
};
}
private BinaryOperator<String> joinWith(String delimeter) {
return (prefix, name) -> {
StringBuilder joined = new StringBuilder(prefix.length() + delimeter.length() + name.length());
joined.append(prefix);
if (!prefix.isEmpty() && !prefix.endsWith(delimeter) && !name.startsWith(delimeter)) {
joined.append(delimeter);
}
joined.append(name);
return joined.toString();
};
}
/**
* Callback used to add pairs.
*
* @param <T> the item type
*/
public class Pairs<T> {
private final BinaryOperator<String> joiner;
private final List<BiConsumer<T, BiConsumer<String, ?>>> addedPairs;
Pairs(BinaryOperator<String> joiner, Consumer<Pairs<T>> pairs) {
this.joiner = joiner;
this.addedPairs = new ArrayList<>();
pairs.accept(this);
}
/**
* Add pairs from map entries.
* @param <K> the map key type
* @param <V> the map value type
* @param extractor the extractor used to provide the map
*/
public <K, V> void addMapEntries(Function<T, Map<String, V>> extractor) {
add(extractor.andThen(Map::entrySet), Map.Entry::getKey, Map.Entry::getValue);
}
/**
* Add pairs from an iterable.
* @param elementsExtractor the extractor used to provide the iterable
* @param pairExtractor the extractor used to provide the name and value
* @param <E> the element type
* @param <V> the value type
*/
public <E, V> void add(Function<T, Iterable<E>> elementsExtractor, PairExtractor<E> pairExtractor) {
add(elementsExtractor, pairExtractor::getName, pairExtractor::getValue);
}
/**
* Add pairs from an iterable.
* @param elementsExtractor the extractor used to provide the iterable
* @param <E> the element type
* @param <V> the value type
* @param nameExtractor the extractor used to provide the name
* @param valueExtractor the extractor used to provide the value
*/
public <E, V> void add(Function<T, Iterable<E>> elementsExtractor, Function<E, String> nameExtractor,
Function<E, V> valueExtractor) {
add((item, pairs) -> {
Iterable<E> elements = elementsExtractor.apply(item);
if (elements != null) {
elements.forEach((element) -> {
String name = nameExtractor.apply(element);
V value = valueExtractor.apply(element);
pairs.accept(name, value);
});
}
});
}
/**
* Add pairs using the given callback.
* @param <V> the value type
* @param pairs callback provided with the item and consumer that can be called to
* actually add the pairs
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <V> void add(BiConsumer<T, BiConsumer<String, V>> pairs) {
this.addedPairs.add((BiConsumer) pairs);
}
void flat(T item, BiConsumer<String, Object> pairs) {
this.addedPairs.forEach((action) -> action.accept(item, joining(pairs)));
}
@SuppressWarnings("unchecked")
void nested(T item, BiConsumer<String, Object> pairs) {
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
this.addedPairs.forEach((addedPair) -> {
addedPair.accept(item, joining((name, value) -> {
List<String> nameParts = List.of(name.split("\\."));
Map<String, Object> destination = result;
for (int i = 0; i < nameParts.size() - 1; i++) {
Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>());
if (!(existing instanceof Map)) {
String common = nameParts.subList(0, i + 1).stream().collect(Collectors.joining("."));
throw new IllegalStateException(
"Duplicate nested pairs added under '%s'".formatted(common));
}
destination = (Map<String, Object>) existing;
}
Object previous = destination.put(nameParts.get(nameParts.size() - 1), value);
Assert.state(previous == null, () -> "Duplicate nested pairs added under '%s'".formatted(name));
}));
});
result.forEach(pairs::accept);
}
private <V> BiConsumer<String, V> joining(BiConsumer<String, V> pairs) {
return (name, value) -> {
name = this.joiner.apply(ContextPairs.this.prefix, (name != null) ? name : "");
if (StringUtils.hasLength(name)) {
pairs.accept(name, value);
}
};
}
}
}

View File

@ -1,63 +0,0 @@
/*
* Copyright 2012-2025 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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
/**
* Utility to help with writing ElasticCommonSchema pairs in their nested form.
*
* @author Phillip Webb
* @since 3.5.0
*/
public final class ElasticCommonSchemaPairs {
private ElasticCommonSchemaPairs() {
}
public static Map<String, Object> nested(Map<String, String> map) {
return nested(map::forEach);
}
@SuppressWarnings("unchecked")
public static <K, V> Map<String, Object> nested(Consumer<BiConsumer<K, V>> nested) {
Map<String, Object> result = new LinkedHashMap<>();
nested.accept((name, value) -> {
List<String> nameParts = List.of(name.toString().split("\\."));
Map<String, Object> destination = result;
for (int i = 0; i < nameParts.size() - 1; i++) {
Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>());
if (!(existing instanceof Map)) {
String common = nameParts.subList(0, i + 1).stream().collect(Collectors.joining("."));
throw new IllegalStateException("Duplicate ECS pairs added under '%s'".formatted(common));
}
destination = (Map<String, Object>) existing;
}
Object previous = destination.put(nameParts.get(nameParts.size() - 1), value);
Assert.state(previous == null, () -> "Duplicate ECS pairs added under '%s'".formatted(name));
});
return result;
}
}

View File

@ -31,6 +31,7 @@ import org.springframework.core.env.Environment;
* <li>{@link Environment}</li>
* <li>{@link StructuredLoggingJsonMembersCustomizer}</li>
* <li>{@link StackTracePrinter} (may be {@code null})</li>
* <li>{@link ContextPairs}</li>
* </ul>
* When using Logback, implementing classes can also use the following parameter types in
* the constructor:

View File

@ -25,6 +25,7 @@ import java.util.function.Consumer;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.Context;
import org.springframework.boot.util.Instantiator;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.boot.util.Instantiator.FailureHandler;
@ -86,6 +87,7 @@ public class StructuredLogFormatterFactory<E> {
allAvailableParameters.add(StructuredLoggingJsonMembersCustomizer.class,
(type) -> getStructuredLoggingJsonMembersCustomizer(properties));
allAvailableParameters.add(StackTracePrinter.class, (type) -> getStackTracePrinter(properties));
allAvailableParameters.add(ContextPairs.class, (type) -> getContextPairs(properties));
if (availableParameters != null) {
availableParameters.accept(allAvailableParameters);
}
@ -122,6 +124,12 @@ public class StructuredLogFormatterFactory<E> {
return (properties != null && properties.stackTrace() != null) ? properties.stackTrace().createPrinter() : null;
}
private ContextPairs getContextPairs(StructuredLoggingJsonProperties properties) {
Context contextProperties = (properties != null) ? properties.context() : null;
contextProperties = (contextProperties != null) ? contextProperties : new Context(true, null);
return new ContextPairs(contextProperties.include(), contextProperties.prefix());
}
/**
* Get a new {@link StructuredLogFormatter} instance for the specified format.
* @param format the format requested (either a {@link CommonStructuredLogFormat} ID

View File

@ -32,6 +32,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.StandardStackTracePrinter;
import org.springframework.boot.util.Instantiator;
@ -47,11 +48,12 @@ import org.springframework.core.env.Environment;
* @param stackTrace stack trace properties
* @param customizer the fully qualified names of
* {@link StructuredLoggingJsonMembersCustomizer} implementations
* @param context context specific properties
* @author Phillip Webb
* @author Yanming Zhou
*/
record StructuredLoggingJsonProperties(Set<String> include, Set<String> exclude, Map<String, String> rename,
Map<String, String> add, StackTrace stackTrace,
Map<String, String> add, StackTrace stackTrace, Context context,
Set<Class<? extends StructuredLoggingJsonMembersCustomizer<?>>> customizer) {
StructuredLoggingJsonProperties {
@ -148,6 +150,18 @@ record StructuredLoggingJsonProperties(Set<String> include, Set<String> exclude,
}
/**
* Properties that influence context values (usually elements propagated from the
* logging MDC).
*
* @param include if context elements should be included
* @param prefix the prefix to use for context elements
* @since 3.5.0
*/
record Context(@DefaultValue("true") boolean include, String prefix) {
}
static class StructuredLoggingJsonPropertiesRuntimeHints implements RuntimeHintsRegistrar {
@Override

View File

@ -266,6 +266,16 @@
"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.context.include",
"type": "java.lang.Boolean",
"description": "Whether context data should be included in the JSON."
},
{
"name": "logging.structured.json.context.prefix",
"type": "java.lang.String",
"description": "The prefix to use when inserting context data."
},
{
"name": "logging.structured.json.customizer",
"type": "java.util.Set<java.lang.Class<? extends org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer<?>>>",

View File

@ -28,6 +28,7 @@ import org.apache.logging.log4j.message.MapMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.TestContextPairs;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@ -54,7 +55,8 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
this.environment.setProperty("logging.structured.ecs.service.environment", "test");
this.environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, null, this.customizer);
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, null,
TestContextPairs.include(), this.customizer);
}
@Test
@ -107,7 +109,7 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
@SuppressWarnings("unchecked")
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(),
this.customizer);
TestContextPairs.include(), this.customizer);
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
Map<String, Object> deserialized = deserialize(this.formatter.format(event));

View File

@ -24,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.logging.structured.TestContextPairs;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.mock.env.MockEnvironment;
@ -52,7 +53,8 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
this.environment.setProperty("logging.structured.gelf.host", "name");
this.environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment, null, this.customizer);
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment, null,
TestContextPairs.include(), this.customizer);
}
@Test
@ -154,7 +156,7 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
@Test
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment,
new SimpleStackTracePrinter(), this.customizer);
new SimpleStackTracePrinter(), TestContextPairs.include(), this.customizer);
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
String json = this.formatter.format(event);

View File

@ -29,6 +29,8 @@ import org.apache.logging.log4j.message.MapMessage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.logging.structured.TestContextPairs;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
@ -45,7 +47,7 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
@BeforeEach
void setUp() {
this.formatter = new LogstashStructuredLogFormatter(null, this.customizer);
this.formatter = new LogstashStructuredLogFormatter(null, TestContextPairs.include(), this.customizer);
}
@Test
@ -88,7 +90,8 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
@Test
void shouldFormatExceptionWithStackTracePrinter() {
this.formatter = new LogstashStructuredLogFormatter(new SimpleStackTracePrinter(), this.customizer);
this.formatter = new LogstashStructuredLogFormatter(new SimpleStackTracePrinter(), TestContextPairs.include(),
this.customizer);
MutableLogEvent event = createEvent();
event.setThrown(new RuntimeException("Boom"));
String json = this.formatter.format(event);

View File

@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.springframework.boot.logging.structured.TestContextPairs;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
@ -57,7 +58,7 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
this.environment.setProperty("logging.structured.ecs.service.node-name", "node-1");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, null,
getThrowableProxyConverter(), this.customizer);
TestContextPairs.include(), getThrowableProxyConverter(), this.customizer);
}
@Test
@ -114,7 +115,7 @@ class ElasticCommonSchemaStructuredLogFormatterTests extends AbstractStructuredL
@SuppressWarnings("unchecked")
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new ElasticCommonSchemaStructuredLogFormatter(this.environment, new SimpleStackTracePrinter(),
getThrowableProxyConverter(), this.customizer);
TestContextPairs.include(), getThrowableProxyConverter(), this.customizer);
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));

View File

@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.logging.structured.TestContextPairs;
import org.springframework.boot.testsupport.system.CapturedOutput;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.mock.env.MockEnvironment;
@ -56,7 +57,7 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
this.environment.setProperty("logging.structured.gelf.service.version", "1.0.0");
this.environment.setProperty("spring.application.pid", "1");
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment, null,
getThrowableProxyConverter(), this.customizer);
TestContextPairs.include(), getThrowableProxyConverter(), this.customizer);
}
@Test
@ -158,7 +159,8 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
@Test
void shouldFormatExceptionUsingStackTracePrinter() {
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(this.environment,
new SimpleStackTracePrinter(), getThrowableProxyConverter(), this.customizer);
new SimpleStackTracePrinter(), TestContextPairs.include(), getThrowableProxyConverter(),
this.customizer);
LoggingEvent event = createEvent();
event.setMDCPropertyMap(Collections.emptyMap());
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));

View File

@ -29,6 +29,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Marker;
import org.springframework.boot.logging.structured.TestContextPairs;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
@ -46,7 +48,8 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
@BeforeEach
void setUp() {
super.setUp();
this.formatter = new LogstashStructuredLogFormatter(null, getThrowableProxyConverter(), this.customizer);
this.formatter = new LogstashStructuredLogFormatter(null, TestContextPairs.include(),
getThrowableProxyConverter(), this.customizer);
}
@Test
@ -92,8 +95,8 @@ class LogstashStructuredLogFormatterTests extends AbstractStructuredLoggingTests
@Test
void shouldFormatExceptionWithStackTracePrinter() {
this.formatter = new LogstashStructuredLogFormatter(new SimpleStackTracePrinter(), getThrowableProxyConverter(),
this.customizer);
this.formatter = new LogstashStructuredLogFormatter(new SimpleStackTracePrinter(), TestContextPairs.include(),
getThrowableProxyConverter(), this.customizer);
LoggingEvent event = createEvent();
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom")));
event.setMDCPropertyMap(Collections.emptyMap());

View File

@ -0,0 +1,172 @@
/*
* Copyright 2012-2025 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.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link ContextPairs}.
*
* @author Phillip Webb
*/
class ContextPairsTests {
@Test
void flatWhenIncludeFalseDoesNothing() {
ContextPairs contextPairs = new ContextPairs(false, null);
Map<String, String> map = Map.of("spring", "boot");
Map<String, Object> actual = apply(contextPairs.flat(".", (pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual.isEmpty());
}
@Test
void flatIncludesName() {
ContextPairs contextPairs = new ContextPairs(true, null);
Map<String, String> map = Map.of("spring", "boot");
Map<String, Object> actual = apply(contextPairs.flat(".", (pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual).containsExactlyEntriesOf(map);
}
@Test
void flatWhenPrefixAppliesPrefix() {
ContextPairs contextPairs = new ContextPairs(true, "the");
Map<String, String> map = Map.of("spring", "boot");
Map<String, Object> actual = apply(contextPairs.flat("_", (pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual).containsOnly(entry("the_spring", "boot"));
}
@Test
void flatWhenPrefixEndingWithDelimeterAppliesPrefix() {
ContextPairs contextPairs = new ContextPairs(true, "the_");
Map<String, String> map = Map.of("spring", "boot");
Map<String, Object> actual = apply(contextPairs.flat("_", (pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual).containsOnly(entry("the_spring", "boot"));
}
@Test
void flatWhenPrefixAndNameStartingWithDelimeterAppliesPrefix() {
ContextPairs contextPairs = new ContextPairs(true, "the");
Map<String, String> map = Map.of("_spring", "boot");
Map<String, Object> actual = apply(contextPairs.flat("_", (pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual).containsOnly(entry("the_spring", "boot"));
}
@Test
void flatWhenJoinerJoins() {
ContextPairs contextPairs = new ContextPairs(true, "the");
Map<String, String> map = Map.of("spring", "boot");
Map<String, Object> actual = apply(
contextPairs.flat((prefix, name) -> prefix + name, (pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual).containsOnly(entry("thespring", "boot"));
}
@Test
void flatWhenJoinerReturnsNullFilters() {
ContextPairs contextPairs = new ContextPairs(true, "the");
Map<String, String> map = Map.of("spring", "boot");
Map<String, Object> actual = apply(
contextPairs.flat((prefix, name) -> null, (pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual).isEmpty();
}
@Test
void nestedWhenIncludeFalseDoesNothing() {
ContextPairs contextPairs = new ContextPairs(false, null);
Map<String, String> map = Map.of("spring", "boot");
Map<String, Object> actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map)));
assertThat(actual.isEmpty());
}
@Test
void nestedExpandsNames() {
ContextPairs contextPairs = new ContextPairs(true, null);
Map<String, String> map = new LinkedHashMap<>();
map.put("a1.b1.c1", "A1B1C1");
map.put("a1.b2.c1", "A1B2C1");
map.put("a1.b1.c2", "A1B1C2");
Map<String, Object> actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map)));
Map<String, Object> expected = new LinkedHashMap<>();
Map<String, Object> a1 = new LinkedHashMap<>();
Map<String, Object> b1 = new LinkedHashMap<>();
Map<String, Object> b2 = new LinkedHashMap<>();
expected.put("a1", a1);
a1.put("b1", b1);
a1.put("b2", b2);
b1.put("c1", "A1B1C1");
b1.put("c2", "A1B1C2");
b2.put("c1", "A1B2C1");
assertThat(actual).isEqualTo(expected);
}
@Test
void nestedWhenDuplicateInParentThrowsException() {
ContextPairs contextPairs = new ContextPairs(true, null);
Map<String, String> map = new LinkedHashMap<>();
map.put("a1.b1.c1", "A1B1C1");
map.put("a1.b1", "A1B1");
assertThatIllegalStateException()
.isThrownBy(() -> apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map))))
.withMessage("Duplicate nested pairs added under 'a1.b1'");
}
@Test
void nestedWhenDuplicateInLeafThrowsException() {
ContextPairs contextPairs = new ContextPairs(true, null);
Map<String, String> map = new LinkedHashMap<>();
map.put("a1.b1", "A1B1");
map.put("a1.b1.c1", "A1B1C1");
assertThatIllegalStateException()
.isThrownBy(() -> apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map))))
.withMessage("Duplicate nested pairs added under 'a1.b1'");
}
@Test
void nestedWhenPrefixAppliesPrefix() {
ContextPairs contextPairs = new ContextPairs(true, "a1");
Map<String, String> map = new LinkedHashMap<>();
map.put("b1.c1", "A1B1C1");
map.put("b2.c1", "A1B2C1");
map.put("b1.c2", "A1B1C2");
Map<String, Object> actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map)));
Map<String, Object> expected = new LinkedHashMap<>();
Map<String, Object> a1 = new LinkedHashMap<>();
Map<String, Object> b1 = new LinkedHashMap<>();
Map<String, Object> b2 = new LinkedHashMap<>();
expected.put("a1", a1);
a1.put("b1", b1);
a1.put("b2", b2);
b1.put("c1", "A1B1C1");
b1.put("c2", "A1B1C2");
b2.put("c1", "A1B2C1");
assertThat(actual).isEqualTo(expected);
}
Map<String, Object> apply(BiConsumer<?, BiConsumer<String, Object>> action) {
Map<String, Object> result = new LinkedHashMap<>();
action.accept(null, result::put);
return result;
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright 2012-2025 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.LinkedHashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ElasticCommonSchemaPairs}.
*
* @author Phillip Webb
*/
class ElasticCommonSchemaPairsTests {
@Test
void nestedExpandsNames() {
Map<String, String> map = Map.of("a1.b1.c1", "A1B1C1", "a1.b2.c1", "A1B2C1", "a1.b1.c2", "A1B1C2");
Map<String, Object> expected = new LinkedHashMap<>();
Map<String, Object> a1 = new LinkedHashMap<>();
Map<String, Object> b1 = new LinkedHashMap<>();
Map<String, Object> b2 = new LinkedHashMap<>();
expected.put("a1", a1);
a1.put("b1", b1);
a1.put("b2", b2);
b1.put("c1", "A1B1C1");
b1.put("c2", "A1B1C2");
b2.put("c1", "A1B2C1");
assertThat(ElasticCommonSchemaPairs.nested(map)).isEqualTo(expected);
}
@Test
void nestedWhenDuplicateInParentThrowsException() {
Map<String, String> map = new LinkedHashMap<>();
map.put("a1.b1.c1", "A1B1C1");
map.put("a1.b1", "A1B1");
assertThatIllegalStateException().isThrownBy(() -> ElasticCommonSchemaPairs.nested(map))
.withMessage("Duplicate ECS pairs added under 'a1.b1'");
}
@Test
void nestedWhenDuplicateInLeafThrowsException() {
Map<String, String> map = new LinkedHashMap<>();
map.put("a1.b1", "A1B1");
map.put("a1.b1.c1", "A1B1C1");
assertThatIllegalStateException().isThrownBy(() -> ElasticCommonSchemaPairs.nested(map))
.withMessage("Duplicate ECS pairs added under 'a1.b1'");
}
}

View File

@ -43,12 +43,13 @@ import static org.mockito.BDDMockito.given;
class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Mock
private Instantiator<?> instantiator;
@SuppressWarnings("rawtypes")
private Instantiator instantiator;
@Test
void customizeWhenHasExcludeFiltersMember() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null, null);
Set.of("a"), Collections.emptyMap(), Collections.emptyMap(), null, null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).doesNotContain("a").contains("b");
@ -57,7 +58,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasIncludeFiltersOtherMembers() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a"),
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null);
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("a")
@ -69,7 +70,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasIncludeAndExcludeFiltersMembers() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Set.of("a", "b"), Set.of("b"),
Collections.emptyMap(), Collections.emptyMap(), null, null);
Collections.emptyMap(), Collections.emptyMap(), null, null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("a")
@ -81,7 +82,7 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasRenameRenamesMember() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null, null);
Collections.emptySet(), Map.of("a", "z"), Collections.emptyMap(), null, null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("\"z\":\"a\"");
@ -90,20 +91,20 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
@Test
void customizeWhenHasAddAddsMemeber() {
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null, null);
Collections.emptySet(), Collections.emptyMap(), Map.of("z", "z"), null, null, null);
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
assertThat(writeSampleJson(customizer)).contains("\"z\":\"z\"");
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
@SuppressWarnings("unchecked")
void customizeWhenHasCustomizerCustomizesMember() {
StructuredLoggingJsonMembersCustomizer<?> uppercaseCustomizer = (members) -> members
.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
given(((Instantiator) this.instantiator).instantiateType(TestCustomizer.class)).willReturn(uppercaseCustomizer);
given(this.instantiator.instantiateType(TestCustomizer.class)).willReturn(uppercaseCustomizer);
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null,
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null,
Set.of(TestCustomizer.class));
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);
@ -111,12 +112,12 @@ class StructuredLoggingJsonPropertiesJsonMembersCustomizerTests {
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
@SuppressWarnings("unchecked")
void customizeWhenHasCustomizersCustomizesMember() {
given(((Instantiator) this.instantiator).instantiateType(FooCustomizer.class)).willReturn(new FooCustomizer());
given(((Instantiator) this.instantiator).instantiateType(BarCustomizer.class)).willReturn(new BarCustomizer());
given(this.instantiator.instantiateType(FooCustomizer.class)).willReturn(new FooCustomizer());
given(this.instantiator.instantiateType(BarCustomizer.class)).willReturn(new BarCustomizer());
StructuredLoggingJsonProperties properties = new StructuredLoggingJsonProperties(Collections.emptySet(),
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null,
Collections.emptySet(), Collections.emptyMap(), Collections.emptyMap(), null, null,
Set.of(FooCustomizer.class, BarCustomizer.class));
StructuredLoggingJsonPropertiesJsonMembersCustomizer customizer = new StructuredLoggingJsonPropertiesJsonMembersCustomizer(
this.instantiator, properties);

View File

@ -31,6 +31,7 @@ import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.logging.StackTracePrinter;
import org.springframework.boot.logging.StandardStackTracePrinter;
import org.springframework.boot.logging.TestException;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.Context;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StackTrace;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StackTrace.Root;
import org.springframework.boot.logging.structured.StructuredLoggingJsonProperties.StructuredLoggingJsonPropertiesRuntimeHints;
@ -52,7 +53,7 @@ class StructuredLoggingJsonPropertiesTests {
setupJsonProperties(environment);
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"), null, Set.of(TestCustomizer.class)));
Map.of("e", "f"), Map.of("g", "h"), null, null, Set.of(TestCustomizer.class)));
}
@Test
@ -91,12 +92,15 @@ class StructuredLoggingJsonPropertiesTests {
assertThat(RuntimeHintsPredicates.reflection().onType(StructuredLoggingJsonProperties.class)).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection()
.onConstructor(StructuredLoggingJsonProperties.class.getDeclaredConstructor(Set.class, Set.class, Map.class,
Map.class, StackTrace.class, Set.class))
Map.class, StackTrace.class, Context.class, Set.class))
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection()
.onConstructor(StackTrace.class.getDeclaredConstructor(String.class, Root.class, Integer.class,
Integer.class, Boolean.class, Boolean.class))
.invoke()).accepts(hints);
assertThat(RuntimeHintsPredicates.reflection()
.onConstructor(Context.class.getDeclaredConstructor(boolean.class, String.class))
.invoke()).accepts(hints);
}
@Test

View File

@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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;
/**
* Test access to {@link ContextPairs}.
*
* @author Phillip Webb
*/
public final class TestContextPairs {
private TestContextPairs() {
}
public static ContextPairs include() {
return of(true, null);
}
public static ContextPairs of(boolean include, String prefix) {
return new ContextPairs(include, prefix);
}
}