diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java index b95cff09e3..4361888ecb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java @@ -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 { 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 members) { + ContextPairs contextPairs, JsonWriter.Members 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 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()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java index 562c518c2f..1352e3a591 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatter.java @@ -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 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 members) { + ContextPairs contextPairs, JsonWriter.Members 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 pairs) { - contextData.forEach((name, value) -> createAdditionalField(name, value, pairs)); + private static void addContextDataPairs(ContextPairs.Pairs contextPairs) { + contextPairs.add((contextData, pairs) -> contextData.forEach(pairs::accept)); } - private static void createAdditionalField(String name, Object value, BiConsumer 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 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; + }; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java index 789a27fafa..66c2f9efe1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatter.java @@ -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 { - 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 members) { + private static void jsonMembers(StackTracePrinter stackTracePrinter, ContextPairs contextPairs, + JsonWriter.Members 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 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 contextPairs) { + contextPairs.add((contextData, pairs) -> contextData.forEach(pairs::accept)); + } + private static Set getMarkers(Marker marker) { Set result = new TreeSet<>(); addMarkers(result, marker); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java index 086d6707fe..05bcabb9df 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/StructuredLogLayout.java @@ -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); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java index c2dc78d660..18bf966c8d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java @@ -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 { + private static final PairExtractor 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 members) { + ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter, + JsonWriter.Members 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 nested(List keyValuePairs) { - return ElasticCommonSchemaPairs.nested((nested) -> keyValuePairs - .forEach((keyValuePair) -> nested.accept(keyValuePair.key, keyValuePair.value))); - } - private static Set getMarkers(List markers) { Set result = new TreeSet<>(); addMarkers(result, markers.iterator()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java index 02ed910ef6..ebc47de627 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatter.java @@ -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 { + private static final PairExtractor 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 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 members) { + ContextPairs contextPairs, ThrowableProxyConverter throwableProxyConverter, + JsonWriter.Members 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 keyValuePairs, BiConsumer pairs) { - keyValuePairs.forEach((keyValuePair) -> createAdditionalField(keyValuePair.key, keyValuePair.value, pairs)); - } - - private static void createAdditionalField(String name, Object value, BiConsumer 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 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; + }; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java index 16e0865fd3..d073217d92 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java @@ -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 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 members) { Extractor extractor = new Extractor(stackTracePrinter, throwableProxyConverter); members.add("@timestamp", ILoggingEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp); @@ -65,10 +66,10 @@ class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter { + pairs.addMapEntries(ILoggingEvent::getMDCPropertyMap); + pairs.add(ILoggingEvent::getKeyValuePairs, keyValuePairExtractor); + })); members.add("tags", ILoggingEvent::getMarkerList) .whenNotNull() .as(LogstashStructuredLogFormatter::getMarkers) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java index f72ac17026..8b7b053e3d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/StructuredLogEncoder.java @@ -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 { private StructuredLogFormatter 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 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 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 diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java new file mode 100644 index 0000000000..10d80cf5bb --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java @@ -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 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 BiConsumer> flat(String delimeter, Consumer> pairs) { + return flat(joinWith(delimeter), pairs); + } + + /** + * Add pairs using flat naming. + * @param 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 BiConsumer> flat(BinaryOperator joiner, Consumer> pairs) { + return (!this.include) ? none() : new Pairs<>(joiner, pairs)::flat; + } + + /** + * Add pairs using nested naming (for example as used in ECS). + * @param the item type + * @param pairs callback to add all the pairs + * @return a {@link BiConsumer} for use with the {@link JsonWriter} + */ + public BiConsumer> nested(Consumer> pairs) { + return (!this.include) ? none() : new Pairs<>(joinWith("."), pairs)::nested; + } + + private BiConsumer> none() { + return (item, pairs) -> { + }; + } + + private BinaryOperator 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 the item type + */ + public class Pairs { + + private final BinaryOperator joiner; + + private final List>> addedPairs; + + Pairs(BinaryOperator joiner, Consumer> pairs) { + this.joiner = joiner; + this.addedPairs = new ArrayList<>(); + pairs.accept(this); + } + + /** + * Add pairs from map entries. + * @param the map key type + * @param the map value type + * @param extractor the extractor used to provide the map + */ + public void addMapEntries(Function> 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 the element type + * @param the value type + */ + public void add(Function> elementsExtractor, PairExtractor pairExtractor) { + add(elementsExtractor, pairExtractor::getName, pairExtractor::getValue); + } + + /** + * Add pairs from an iterable. + * @param elementsExtractor the extractor used to provide the iterable + * @param the element type + * @param the value type + * @param nameExtractor the extractor used to provide the name + * @param valueExtractor the extractor used to provide the value + */ + public void add(Function> elementsExtractor, Function nameExtractor, + Function valueExtractor) { + add((item, pairs) -> { + Iterable 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 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 void add(BiConsumer> pairs) { + this.addedPairs.add((BiConsumer) pairs); + } + + void flat(T item, BiConsumer pairs) { + this.addedPairs.forEach((action) -> action.accept(item, joining(pairs))); + } + + @SuppressWarnings("unchecked") + void nested(T item, BiConsumer pairs) { + LinkedHashMap result = new LinkedHashMap<>(); + this.addedPairs.forEach((addedPair) -> { + addedPair.accept(item, joining((name, value) -> { + List nameParts = List.of(name.split("\\.")); + Map 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) 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 BiConsumer joining(BiConsumer pairs) { + return (name, value) -> { + name = this.joiner.apply(ContextPairs.this.prefix, (name != null) ? name : ""); + if (StringUtils.hasLength(name)) { + pairs.accept(name, value); + } + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPairs.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPairs.java deleted file mode 100644 index 4f03466b5b..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPairs.java +++ /dev/null @@ -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 nested(Map map) { - return nested(map::forEach); - } - - @SuppressWarnings("unchecked") - public static Map nested(Consumer> nested) { - Map result = new LinkedHashMap<>(); - nested.accept((name, value) -> { - List nameParts = List.of(name.toString().split("\\.")); - Map 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) 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; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java index 897093d2c3..115ff45f9c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatter.java @@ -31,6 +31,7 @@ import org.springframework.core.env.Environment; *
  • {@link Environment}
  • *
  • {@link StructuredLoggingJsonMembersCustomizer}
  • *
  • {@link StackTracePrinter} (may be {@code null})
  • + *
  • {@link ContextPairs}
  • * * When using Logback, implementing classes can also use the following parameter types in * the constructor: diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java index 67116d54ca..b8b16ef6cc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLogFormatterFactory.java @@ -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 { 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 { 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 diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java index 3ec3131ec3..3485d947da 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/structured/StructuredLoggingJsonProperties.java @@ -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 include, Set exclude, Map rename, - Map add, StackTrace stackTrace, + Map add, StackTrace stackTrace, Context context, Set>> customizer) { StructuredLoggingJsonProperties { @@ -148,6 +150,18 @@ record StructuredLoggingJsonProperties(Set include, Set 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 diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index f8eff0e5bd..9066d1a154 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -266,6 +266,16 @@ "type": "java.util.Map", "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>>", diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java index 11fa3d53fb..b6fc7d04eb 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java @@ -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 deserialized = deserialize(this.formatter.format(event)); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatterTests.java index 73721b0621..d8acf8d27c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/GraylogExtendedLogFormatStructuredLogFormatterTests.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatterTests.java index 8662eea8fd..9f04e4e368 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/LogstashStructuredLogFormatterTests.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java index 0908d433e2..b633f6cd60 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java @@ -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"))); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatterTests.java index 9aeaecc454..1ebaaf8674 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/GraylogExtendedLogFormatStructuredLogFormatterTests.java @@ -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"))); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatterTests.java index c2350db4c6..10de006668 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatterTests.java @@ -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()); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java new file mode 100644 index 0000000000..ef114693ea --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java @@ -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 map = Map.of("spring", "boot"); + Map actual = apply(contextPairs.flat(".", (pairs) -> pairs.addMapEntries((item) -> map))); + assertThat(actual.isEmpty()); + } + + @Test + void flatIncludesName() { + ContextPairs contextPairs = new ContextPairs(true, null); + Map map = Map.of("spring", "boot"); + Map actual = apply(contextPairs.flat(".", (pairs) -> pairs.addMapEntries((item) -> map))); + assertThat(actual).containsExactlyEntriesOf(map); + } + + @Test + void flatWhenPrefixAppliesPrefix() { + ContextPairs contextPairs = new ContextPairs(true, "the"); + Map map = Map.of("spring", "boot"); + Map 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 map = Map.of("spring", "boot"); + Map 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 map = Map.of("_spring", "boot"); + Map 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 map = Map.of("spring", "boot"); + Map 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 map = Map.of("spring", "boot"); + Map 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 map = Map.of("spring", "boot"); + Map actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map))); + assertThat(actual.isEmpty()); + } + + @Test + void nestedExpandsNames() { + ContextPairs contextPairs = new ContextPairs(true, null); + Map map = new LinkedHashMap<>(); + map.put("a1.b1.c1", "A1B1C1"); + map.put("a1.b2.c1", "A1B2C1"); + map.put("a1.b1.c2", "A1B1C2"); + Map actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map))); + Map expected = new LinkedHashMap<>(); + Map a1 = new LinkedHashMap<>(); + Map b1 = new LinkedHashMap<>(); + Map 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 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 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 map = new LinkedHashMap<>(); + map.put("b1.c1", "A1B1C1"); + map.put("b2.c1", "A1B2C1"); + map.put("b1.c2", "A1B1C2"); + Map actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map))); + Map expected = new LinkedHashMap<>(); + Map a1 = new LinkedHashMap<>(); + Map b1 = new LinkedHashMap<>(); + Map 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 apply(BiConsumer> action) { + Map result = new LinkedHashMap<>(); + action.accept(null, result::put); + return result; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPairsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPairsTests.java deleted file mode 100644 index 8d9fff340a..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/ElasticCommonSchemaPairsTests.java +++ /dev/null @@ -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 map = Map.of("a1.b1.c1", "A1B1C1", "a1.b2.c1", "A1B2C1", "a1.b1.c2", "A1B1C2"); - Map expected = new LinkedHashMap<>(); - Map a1 = new LinkedHashMap<>(); - Map b1 = new LinkedHashMap<>(); - Map 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 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 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'"); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java index 2c28f87023..e9e9d755ab 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesJsonMembersCustomizerTests.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java index e283199b2e..b68246f8ee 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/StructuredLoggingJsonPropertiesTests.java @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/TestContextPairs.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/TestContextPairs.java new file mode 100644 index 0000000000..b3f3b7eed9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/structured/TestContextPairs.java @@ -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); + } + +}