Log correlation IDs when Micrometer tracing is being used

Add support for logging correlation IDs with Logback or Log4J2 whenever
Micrometer tracing is being used.

The `LoggingSystemProperties` class now accepts a defualt value resolver
which will be used whenever a value isn't in the environment. The
`AbstractLoggingSystem` provides a resolver that supports the
`logging.pattern.correlation` property and will return a value whenever
`LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY` is set.

Using `LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY` allows us to
provide a consistent width for the correlation ID, even when it's
missing from the MDC.

The exact correlation pattern returned will depend on the `LoggingSytem`
implementation. Currently Logback and Log4J2 are supported and both
make use of a custom converter which delegates to a new
`CorrelationIdFormatter` class.

Closes gh-33280
pull/36032/head
Jonatan Ivanov 1 year ago committed by Phillip Webb
parent b6120d504a
commit c1b295fd71

@ -0,0 +1,69 @@
/*
* Copyright 2012-2023 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.actuate.autoconfigure.tracing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
/**
* {@link EnvironmentPostProcessor} to add a {@link PropertySource} to support log
* correlation IDs when Micrometer is present. Adds support for the
* {@value LoggingSystem#EXPECT_CORRELATION_ID_PROPERTY} property by delegating to
* {@code management.tracing.enabled}.
*
* @author Jonatan Ivanov
* @author Phillip Webb
*/
class LogCorrelationEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (ClassUtils.isPresent("io.micrometer.tracing.Tracer", application.getClassLoader())) {
environment.getPropertySources().addLast(new LogCorrelationPropertySource(this, environment));
}
}
/**
* Log correlation {@link PropertySource}.
*/
private static class LogCorrelationPropertySource extends PropertySource<Object> {
private static final String NAME = "logCorrelation";
private final Environment environment;
LogCorrelationPropertySource(Object source, Environment environment) {
super(NAME, source);
this.environment = environment;
}
@Override
public Object getProperty(String name) {
if (name.equals(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY)) {
return this.environment.getProperty("management.tracing.enabled", Boolean.class, Boolean.TRUE);
}
return null;
}
}
}

@ -1,3 +1,7 @@
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.actuate.autoconfigure.tracing.LogCorrelationEnvironmentPostProcessor

@ -0,0 +1,67 @@
/*
* Copyright 2012-2023 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.actuate.autoconfigure.tracing;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LogCorrelationEnvironmentPostProcessor}.
*
* @author Jonatan Ivanov
* @author Phillip Webb
*/
class LogCorrelationEnvironmentPostProcessorTests {
private final ConfigurableEnvironment environment = new StandardEnvironment();
private final SpringApplication application = new SpringApplication();
private final LogCorrelationEnvironmentPostProcessor postProcessor = new LogCorrelationEnvironmentPostProcessor();
@Test
void getExpectCorrelationIdPropertyWhenMicrometerPresentReturnsTrue() {
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false))
.isTrue();
}
@Test
@ClassPathExclusions("micrometer-tracing-*.jar")
void getExpectCorrelationIdPropertyWhenMicrometerMissingReturnsFalse() {
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false))
.isFalse();
}
@Test
void getExpectCorrelationIdPropertyWhenTracingDisabledReturnsFalse() {
TestPropertyValues.of("management.tracing.enabled=false").applyTo(this.environment);
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false))
.isFalse();
}
}

@ -65,7 +65,25 @@ Now open the Zipkin UI at `http://localhost:9411` and press the "Run Query" butt
You should see one trace.
Press the "Show" button to see the details of that trace.
TIP: You can include the current trace and span id in the logs by setting the `logging.pattern.level` property to `%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]`
[[actuator.micrometer-tracing.logging]]
=== Logging Correlation IDs
Correlation IDs provide a helpful way to link lines in your log files to distributed traces.
By default, as long as configprop:management.tracing.enabled[] has not been set to `false`, Spring Boot will include correlation IDs in your logs whenever you are using Micrometer tracing.
The default correlation ID is built from `traceId` and `spanId` https://logback.qos.ch/manual/mdc.html[MDC] values.
For example, if Micrometer tracing has added an MDC `traceId` of `803B448A0489F84084905D3093480352` and an MDC `spanId` of `3425F23BB2432450` the log output will include the correlation ID `[803B448A0489F84084905D3093480352-3425F23BB2432450]`.
If you prefer to use a different format for your correlation ID, you can use the configprop:logging.pattern.correlation[] property to define one.
For example, the following will provide a correlation ID for Logback in format previously used by Spring Sleuth:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
----
logging:
pattern:
correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
----

@ -32,6 +32,7 @@ The following items are output:
* Process ID.
* A `---` separator to distinguish the start of actual log messages.
* Thread name: Enclosed in square brackets (may be truncated for console output).
* Correlation ID: If tracing is enabled (not shown in the sample above)
* Logger name: This is usually the source class name (often abbreviated).
* The log message.

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2023 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.
@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
@ -174,7 +175,35 @@ public abstract class AbstractLoggingSystem extends LoggingSystem {
}
protected final void applySystemProperties(Environment environment, LogFile logFile) {
new LoggingSystemProperties(environment).apply(logFile);
new LoggingSystemProperties(environment, getDefaultValueResolver(environment), null).apply(logFile);
}
/**
* Return the default value resolver to use when resolving system properties.
* @param environment the environment
* @return the default value resolver
* @since 3.2.0
*/
protected Function<String, String> getDefaultValueResolver(Environment environment) {
String defaultLogCorrelationPattern = getDefaultLogCorrelationPattern();
return (name) -> {
if (StringUtils.hasLength(defaultLogCorrelationPattern)
&& LoggingSystemProperty.CORRELATION_PATTERN.getApplicationPropertyName().equals(name)
&& environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false)) {
return defaultLogCorrelationPattern;
}
return null;
};
}
/**
* Return the default log correlation pattern or {@code null} if log correlation
* patterns are not supported.
* @return the default log correlation pattern
* @since 3.2.0
*/
protected String getDefaultLogCorrelationPattern() {
return null;
}
/**

@ -0,0 +1,199 @@
/*
* Copyright 2012-2023 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;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Utility class that can be used to format a correlation identifier for logging based on
* <a href=
* "https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers">w3c</a>
* recommendations.
* <p>
* The formatter can be configured with a comma-separated list of names and the expected
* length of their resolved value. Each item should be specified in the form
* {@code "<name>(length)"}. For example, {@code "traceId(32),spanId(16)"} specifies the
* names {@code "traceId"} and {@code "spanId"} with expected lengths of {@code 32} and
* {@code 16} respectively.
* <p>
* Correlation IDs are formatted as dash separated strings surrounded in square brackets.
* Formatted output is always of a fixed width and with trailing whitespace. Dashes are
* omitted of none of the named items can be resolved.
* <p>
* The following example would return a formatted result of
* {@code "[01234567890123456789012345678901-0123456789012345] "}: <pre class="code">
* CorrelationIdFormatter formatter = CorrelationIdFormatter.of("traceId(32),spanId(16)");
* Map&lt;String, String&gt; mdc = Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345");
* return formatter.format(mdc::get);
* </pre>
* <p>
* If {@link #of(String)} is called with an empty spec the {@link #DEFAULT} formatter will
* be used.
*
* @author Phillip Webb
* @since 3.2.0
* @see #of(String)
* @see #of(Collection)
*/
public final class CorrelationIdFormatter {
/**
* Default {@link CorrelationIdFormatter}.
*/
public static final CorrelationIdFormatter DEFAULT = CorrelationIdFormatter.of("traceId(32),spanId(16)");
private final List<Part> parts;
private final String blank;
private CorrelationIdFormatter(List<Part> parts) {
this.parts = parts;
this.blank = String.format("[%s] ", parts.stream().map(Part::blank).collect(Collectors.joining(" ")));
}
/**
* Format a correlation from the values in the given resolver.
* @param resolver the resolver used to resolve named values
* @return a formatted correlation id
*/
public String format(Function<String, String> resolver) {
StringBuilder result = new StringBuilder();
formatTo(resolver, result);
return result.toString();
}
/**
* Format a correlation from the values in the given resolver and append it to the
* given {@link Appendable}.
* @param resolver the resolver used to resolve named values
* @param appendable the appendable for the formatted correlation id
*/
public void formatTo(Function<String, String> resolver, Appendable appendable) {
Predicate<Part> canResolve = (part) -> StringUtils.hasLength(resolver.apply(part.name()));
try {
if (this.parts.stream().anyMatch(canResolve)) {
appendable.append("[");
for (Iterator<Part> iterator = this.parts.iterator(); iterator.hasNext();) {
appendable.append(iterator.next().resolve(resolver));
appendable.append((!iterator.hasNext()) ? "" : "-");
}
appendable.append("] ");
}
else {
appendable.append(this.blank);
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
@Override
public String toString() {
return this.parts.stream().map(Part::toString).collect(Collectors.joining(","));
}
/**
* Create a new {@link CorrelationIdFormatter} instance from the given specification.
* @param spec a comma separated specification
* @return a new {@link CorrelationIdFormatter} instance
*/
public static CorrelationIdFormatter of(String spec) {
try {
return (!StringUtils.hasText(spec)) ? DEFAULT : of(List.of(spec.split(",")));
}
catch (Exception ex) {
throw new IllegalStateException("Unable to parse correlation formatter spec '%s'".formatted(spec), ex);
}
}
/**
* Create a new {@link CorrelationIdFormatter} instance from the given specification.
* @param spec a pre-separated specification
* @return a new {@link CorrelationIdFormatter} instance
*/
public static CorrelationIdFormatter of(String[] spec) {
return of((spec != null) ? Arrays.asList(spec) : Collections.emptyList());
}
/**
* Create a new {@link CorrelationIdFormatter} instance from the given specification.
* @param spec a pre-separated specification
* @return a new {@link CorrelationIdFormatter} instance
*/
public static CorrelationIdFormatter of(Collection<String> spec) {
if (CollectionUtils.isEmpty(spec)) {
return DEFAULT;
}
List<Part> parts = spec.stream().map(Part::of).toList();
return new CorrelationIdFormatter(parts);
}
/**
* A part of the correlation id.
*
* @param name the name of the correlation part
* @param length the expected length of the correlation part
*/
static final record Part(String name, int length) {
private static final Pattern pattern = Pattern.compile("^(.+?)\\((\\d+)\\)?$");
String resolve(Function<String, String> resolver) {
String resolved = resolver.apply(name());
if (resolved == null) {
return blank();
}
int padding = length() - resolved.length();
return resolved + " ".repeat((padding > 0) ? padding : 0);
}
String blank() {
return " ".repeat(this.length);
}
@Override
public String toString() {
return "%s(%s)".formatted(name(), length());
}
static Part of(String part) {
Matcher matcher = pattern.matcher(part.trim());
Assert.state(matcher.matches(), () -> "Invalid specification part '%s'".formatted(part));
String name = matcher.group(1);
int length = Integer.parseInt(matcher.group(2));
return new Part(name, length);
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2023 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.
@ -23,6 +23,7 @@ import java.util.List;
import java.util.Set;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@ -58,6 +59,13 @@ public abstract class LoggingSystem {
private static final LoggingSystemFactory SYSTEM_FACTORY = LoggingSystemFactory.fromSpringFactories();
/**
* The name of an {@link Environment} property used to indicate that a correlation ID
* is expected to be logged at some point.
* @since 3.2.0
*/
public static final String EXPECT_CORRELATION_ID_PROPERTY = "logging.expect-correlation-id";
/**
* Return the {@link LoggingSystemProperties} that should be applied.
* @param environment the {@link ConfigurableEnvironment} used to obtain value

@ -19,6 +19,7 @@ package org.springframework.boot.logging;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.springframework.boot.system.ApplicationPid;
import org.springframework.core.env.ConfigurableEnvironment;
@ -36,6 +37,7 @@ import org.springframework.util.Assert;
* @author Vedran Pavic
* @author Robert Thornton
* @author Eddú Meléndez
* @author Jonatan Ivanov
* @since 2.0.0
* @see LoggingSystemProperty
*/
@ -160,6 +162,8 @@ public class LoggingSystemProperties {
private final Environment environment;
private final Function<String, String> defaultValueResolver;
private final BiConsumer<String, String> setter;
/**
@ -167,20 +171,34 @@ public class LoggingSystemProperties {
* @param environment the source environment
*/
public LoggingSystemProperties(Environment environment) {
this(environment, systemPropertySetter);
this(environment, null);
}
/**
* Create a new {@link LoggingSystemProperties} instance.
* @param environment the source environment
* @param setter setter used to apply the property
* @param setter setter used to apply the property or {@code null} for system
* properties
* @since 2.4.2
*/
public LoggingSystemProperties(Environment environment, BiConsumer<String, String> setter) {
this(environment, null, setter);
}
/**
* Create a new {@link LoggingSystemProperties} instance.
* @param environment the source environment
* @param defaultValueResolver function used to resolve default values or {@code null}
* @param setter setter used to apply the property or {@code null} for system
* properties
* @since 3.2.0
*/
public LoggingSystemProperties(Environment environment, Function<String, String> defaultValueResolver,
BiConsumer<String, String> setter) {
Assert.notNull(environment, "Environment must not be null");
Assert.notNull(setter, "Setter must not be null");
this.environment = environment;
this.setter = setter;
this.defaultValueResolver = (defaultValueResolver != null) ? defaultValueResolver : (name) -> null;
this.setter = (setter != null) ? setter : systemPropertySetter;
}
protected Charset getDefaultCharset() {
@ -219,6 +237,7 @@ public class LoggingSystemProperties {
setSystemProperty(LoggingSystemProperty.FILE_PATTERN, resolver);
setSystemProperty(LoggingSystemProperty.LEVEL_PATTERN, resolver);
setSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN, resolver);
setSystemProperty(LoggingSystemProperty.CORRELATION_PATTERN, resolver);
if (logFile != null) {
logFile.applyToSystemProperties();
}
@ -231,6 +250,7 @@ public class LoggingSystemProperties {
private void setSystemProperty(LoggingSystemProperty property, PropertyResolver resolver, String defaultValue) {
String value = (property.getApplicationPropertyName() != null)
? resolver.getProperty(property.getApplicationPropertyName()) : null;
value = (value != null) ? value : this.defaultValueResolver.apply(property.getApplicationPropertyName());
value = (value != null) ? value : defaultValue;
setSystemProperty(property.getEnvironmentVariableName(), value);
}
@ -263,6 +283,7 @@ public class LoggingSystemProperties {
protected final void setSystemProperty(PropertyResolver resolver, String systemPropertyName, String propertyName,
String defaultValue) {
String value = resolver.getProperty(propertyName);
value = (value != null) ? value : this.defaultValueResolver.apply(systemPropertyName);
value = (value != null) ? value : defaultValue;
setSystemProperty(systemPropertyName, value);
}

@ -83,7 +83,12 @@ public enum LoggingSystemProperty {
/**
* Logging system property for the date-format pattern.
*/
DATEFORMAT_PATTERN("LOG_DATEFORMAT_PATTERN", "logging.pattern.dateformat");
DATEFORMAT_PATTERN("LOG_DATEFORMAT_PATTERN", "logging.pattern.dateformat"),
/**
* Logging system property for the correlation pattern.
*/
CORRELATION_PATTERN("LOG_CORRELATION_PATTERN", "logging.pattern.correlation");
private final String environmentVariableName;

@ -0,0 +1,69 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging.log4j2;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.core.pattern.MdcPatternConverter;
import org.apache.logging.log4j.core.pattern.PatternConverter;
import org.apache.logging.log4j.util.PerformanceSensitive;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.logging.CorrelationIdFormatter;
import org.springframework.util.ObjectUtils;
/**
* Log4j2 {@link LogEventPatternConverter} to convert a {@link CorrelationIdFormatter}
* pattern into formatted output using data from the {@link LogEvent#getContextData()
* MDC}.
*
* @author Phillip Webb
* @since 3.2.0
* @see MdcPatternConverter
*/
@Plugin(name = "CorrelationIdConverter", category = PatternConverter.CATEGORY)
@ConverterKeys("correlationId")
@PerformanceSensitive("allocation")
public final class CorrelationIdConverter extends LogEventPatternConverter {
private final CorrelationIdFormatter formatter;
private CorrelationIdConverter(CorrelationIdFormatter formatter) {
super("correlationId{%s}".formatted(formatter), "mdc");
this.formatter = formatter;
}
@Override
public void format(LogEvent event, StringBuilder toAppendTo) {
ReadOnlyStringMap contextData = event.getContextData();
this.formatter.formatTo(contextData::getValue, toAppendTo);
}
/**
* Factory method to create a new {@link CorrelationIdConverter}.
* @param options options, may be null or first element contains name of property to
* format.
* @return instance of PropertiesPatternConverter.
*/
public static CorrelationIdConverter newInstance(String[] options) {
String pattern = (!ObjectUtils.isEmpty(options)) ? options[0] : null;
return new CorrelationIdConverter(CorrelationIdFormatter.of(pattern));
}
}

@ -249,27 +249,29 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
if (logFile != null) {
loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext));
}
else {
loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile, getOverrides(initializationContext));
}
}
private List<String> getOverrides(LoggingInitializationContext initializationContext) {
BindResult<List<String>> overrides = Binder.get(initializationContext.getEnvironment())
.bind("logging.log4j2.config.override", Bindable.listOf(String.class));
return overrides.orElse(Collections.emptyList());
String location = (logFile != null) ? getPackagedConfigFile("log4j2-file.xml")
: getPackagedConfigFile("log4j2.xml");
load(initializationContext, location, logFile);
}
@Override
protected void loadConfiguration(LoggingInitializationContext initializationContext, String location,
LogFile logFile) {
load(initializationContext, location, logFile);
}
private void load(LoggingInitializationContext initializationContext, String location, LogFile logFile) {
List<String> overrides = getOverrides(initializationContext);
if (initializationContext != null) {
applySystemProperties(initializationContext.getEnvironment(), logFile);
}
loadConfiguration(location, logFile, getOverrides(initializationContext));
loadConfiguration(location, logFile, overrides);
}
private List<String> getOverrides(LoggingInitializationContext initializationContext) {
BindResult<List<String>> overrides = Binder.get(initializationContext.getEnvironment())
.bind("logging.log4j2.config.override", Bindable.listOf(String.class));
return overrides.orElse(Collections.emptyList());
}
/**
@ -492,6 +494,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
loggerContext.setExternalContext(null);
}
@Override
protected String getDefaultLogCorrelationPattern() {
return "%correlationId";
}
/**
* Get the Spring {@link Environment} attached to the given {@link LoggerContext} or
* {@code null} if no environment is available.

@ -0,0 +1,62 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging.logback;
import java.util.Map;
import ch.qos.logback.classic.pattern.MDCConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.DynamicConverter;
import org.springframework.boot.logging.CorrelationIdFormatter;
import org.springframework.core.env.Environment;
/**
* Logback {@link DynamicConverter} to convert a {@link CorrelationIdFormatter} pattern
* into formatted output using data from the {@link ILoggingEvent#getMDCPropertyMap() MDC}
* and {@link Environment}.
*
* @author Phillip Webb
* @since 3.2.0
* @see MDCConverter
*/
public class CorrelationIdConverter extends DynamicConverter<ILoggingEvent> {
private CorrelationIdFormatter formatter;
@Override
public void start() {
this.formatter = CorrelationIdFormatter.of(getOptionList());
super.start();
}
@Override
public void stop() {
this.formatter = null;
super.stop();
}
@Override
public String convert(ILoggingEvent event) {
if (this.formatter == null) {
return "";
}
Map<String, String> mdc = event.getMDCPropertyMap();
return this.formatter.format(mdc::get);
}
}

@ -43,6 +43,7 @@ import org.springframework.boot.logging.LogFile;
* @author Vedran Pavic
* @author Robert Thornton
* @author Scott Frederick
* @author Jonatan Ivanov
*/
class DefaultLogbackConfiguration {
@ -68,12 +69,14 @@ class DefaultLogbackConfiguration {
private void defaults(LogbackConfigurator config) {
config.conversionRule("clr", ColorConverter.class);
config.conversionRule("correlationId", CorrelationIdConverter.class);
config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class);
config.getContext()
.putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${CONSOLE_LOG_PATTERN:-"
+ "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) "
+ "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "
+ "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} "
+ "%clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} "
+ "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));
String defaultCharset = Charset.defaultCharset().name();
config.getContext()
@ -82,6 +85,7 @@ class DefaultLogbackConfiguration {
config.getContext()
.putProperty("FILE_LOG_PATTERN", resolve(config, "${FILE_LOG_PATTERN:-"
+ "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] "
+ "${LOG_CORRELATION_PATTERN:-}"
+ "%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));
config.getContext()
.putProperty("FILE_LOG_CHARSET", resolve(config, "${FILE_LOG_CHARSET:-" + defaultCharset + "}"));

@ -109,7 +109,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
@Override
public LoggingSystemProperties getSystemProperties(ConfigurableEnvironment environment) {
return new LogbackLoggingSystemProperties(environment);
return new LogbackLoggingSystemProperties(environment, getDefaultValueResolver(environment), null);
}
@Override
@ -186,6 +186,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
if (!initializeFromAotGeneratedArtifactsIfPossible(initializationContext, logFile)) {
super.initialize(initializationContext, configLocation, logFile);
}
loggerContext.putObject(Environment.class.getName(), initializationContext.getEnvironment());
loggerContext.getTurboFilterList().remove(FILTER);
markAsInitialized(loggerContext);
if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {
@ -223,7 +224,8 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
}
Environment environment = initializationContext.getEnvironment();
// Apply system properties directly in case the same JVM runs multiple apps
new LogbackLoggingSystemProperties(environment, context::putProperty).apply(logFile);
new LogbackLoggingSystemProperties(environment, getDefaultValueResolver(environment), context::putProperty)
.apply(logFile);
LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
: new LogbackConfigurator(context);
new DefaultLogbackConfiguration(logFile).apply(configurator);
@ -415,6 +417,11 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF
loggerContext.removeObject(LoggingSystem.class.getName());
}
@Override
protected String getDefaultLogCorrelationPattern() {
return "%correlationId";
}
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
String key = BeanFactoryInitializationAotContribution.class.getName();

@ -18,6 +18,7 @@ package org.springframework.boot.logging.logback;
import java.nio.charset.Charset;
import java.util.function.BiConsumer;
import java.util.function.Function;
import ch.qos.logback.core.util.FileSize;
@ -107,6 +108,19 @@ public class LogbackLoggingSystemProperties extends LoggingSystemProperties {
super(environment, setter);
}
/**
* Create a new {@link LoggingSystemProperties} instance.
* @param environment the source environment
* @param defaultValueResolver function used to resolve default values or {@code null}
* @param setter setter used to apply the property or {@code null} for system
* properties
* @since 3.2.0
*/
public LogbackLoggingSystemProperties(Environment environment, Function<String, String> defaultValueResolver,
BiConsumer<String, String> setter) {
super(environment, defaultValueResolver, setter);
}
@Override
protected Charset getDefaultCharset() {
return Charset.defaultCharset();

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 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.
@ -59,7 +59,8 @@ class LogbackRuntimeHints implements RuntimeHintsRegistrar {
private void registerHintsForSpringBootConverters(ReflectionHints reflection) {
registerForPublicConstructorInvocation(reflection, ColorConverter.class,
ExtendedWhitespaceThrowableProxyConverter.class, WhitespaceThrowableProxyConverter.class);
ExtendedWhitespaceThrowableProxyConverter.class, WhitespaceThrowableProxyConverter.class,
CorrelationIdConverter.class);
}
private void registerForPublicConstructorInvocation(ReflectionHints reflection, Class<?>... classes) {

@ -165,6 +165,12 @@
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
"defaultValue": "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
},
{
"name": "logging.pattern.correlation",
"type": "java.lang.String",
"description": "Appender pattern for log correlation. Supported only with the default Logback setup.",
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener"
},
{
"name": "logging.pattern.dateformat",
"type": "java.lang.String",

@ -4,8 +4,8 @@
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd'T'HH:mm:ss.SSSXXX</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="FILE_LOG_PATTERN">%d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="FILE_LOG_PATTERN">%d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">

@ -4,8 +4,8 @@
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd'T'HH:mm:ss.SSSXXX</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="FILE_LOG_PATTERN">%d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="FILE_LOG_PATTERN">%d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">

@ -6,13 +6,14 @@ Default logback configuration provided for import
<included>
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="correlationId" converterClass="org.springframework.boot.logging.logback.CorrelationIdConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
<property name="CONSOLE_LOG_THRESHOLD" value="${CONSOLE_LOG_THRESHOLD:-TRACE}"/>
<property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] ${LOG_CORRELATION_PATTERN:-}%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<property name="FILE_LOG_CHARSET" value="${FILE_LOG_CHARSET:-${file.encoding:-UTF-8}}"/>
<property name="FILE_LOG_THRESHOLD" value="${FILE_LOG_THRESHOLD:-TRACE}"/>

@ -92,6 +92,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* @author Ben Hale
* @author Fahim Farook
* @author Eddú Meléndez
* @author Jonatan Ivanov
*/
@ExtendWith(OutputCaptureExtension.class)
@ClassPathExclusions("log4j*.jar")
@ -467,7 +468,7 @@ class LoggingApplicationListenerTests {
void systemPropertiesAreSetForLoggingConfiguration() {
addPropertiesToEnvironment(this.context, "logging.exception-conversion-word=conversion",
"logging.file.name=" + this.logFile, "logging.file.path=path", "logging.pattern.console=console",
"logging.pattern.file=file", "logging.pattern.level=level",
"logging.pattern.file=file", "logging.pattern.level=level", "logging.pattern.correlation=correlation",
"logging.pattern.rolling-file-name=my.log.%d{yyyyMMdd}.%i.gz");
this.listener.initialize(this.context.getEnvironment(), this.context.getClassLoader());
assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN)).isEqualTo("console");

@ -16,14 +16,19 @@
package org.springframework.boot.logging;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.io.TempDir;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.contentOf;
/**
* Base for {@link LoggingSystem} tests.
*
@ -41,6 +46,7 @@ public abstract class AbstractLoggingSystemTests {
void configureTempDir(@TempDir Path temp) {
this.originalTempDirectory = System.getProperty(JAVA_IO_TMPDIR);
System.setProperty(JAVA_IO_TMPDIR, temp.toAbsolutePath().toString());
MDC.clear();
}
@AfterEach
@ -53,6 +59,7 @@ public abstract class AbstractLoggingSystemTests {
for (LoggingSystemProperty property : LoggingSystemProperty.values()) {
System.getProperties().remove(property.getEnvironmentVariableName());
}
MDC.clear();
}
protected final String[] getSpringConfigLocations(AbstractLoggingSystem system) {
@ -79,4 +86,15 @@ public abstract class AbstractLoggingSystemTests {
return path;
}
protected final String getLineWithText(File file, CharSequence outputSearch) {
return getLineWithText(contentOf(file), outputSearch);
}
protected final String getLineWithText(CharSequence output, CharSequence outputSearch) {
return Arrays.stream(output.toString().split("\\r?\\n"))
.filter((line) -> line.contains(outputSearch))
.findFirst()
.orElse(null);
}
}

@ -0,0 +1,122 @@
/*
* Copyright 2012-2023 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;
import java.util.HashMap;
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 CorrelationIdFormatter}.
*
* @author Phillip Webb
*/
class CorrelationIdFormatterTests {
@Test
void formatWithDefaultSpecWhenHasBothParts() {
Map<String, String> context = new HashMap<>();
context.put("traceId", "01234567890123456789012345678901");
context.put("spanId", "0123456789012345");
String formatted = CorrelationIdFormatter.DEFAULT.format(context::get);
assertThat(formatted).isEqualTo("[01234567890123456789012345678901-0123456789012345] ");
}
@Test
void formatWithDefaultSpecWhenHasNoParts() {
Map<String, String> context = new HashMap<>();
String formatted = CorrelationIdFormatter.DEFAULT.format(context::get);
assertThat(formatted).isEqualTo("[ ] ");
}
@Test
void formatWithDefaultSpecWhenHasOnlyFirstPart() {
Map<String, String> context = new HashMap<>();
context.put("traceId", "01234567890123456789012345678901");
String formatted = CorrelationIdFormatter.DEFAULT.format(context::get);
assertThat(formatted).isEqualTo("[01234567890123456789012345678901- ] ");
}
@Test
void formatWithDefaultSpecWhenHasOnlySecondPart() {
Map<String, String> context = new HashMap<>();
context.put("spanId", "0123456789012345");
String formatted = CorrelationIdFormatter.DEFAULT.format(context::get);
assertThat(formatted).isEqualTo("[ -0123456789012345] ");
}
@Test
void formatWhenPartsAreShort() {
Map<String, String> context = new HashMap<>();
context.put("traceId", "0123456789012345678901234567");
context.put("spanId", "012345678901");
String formatted = CorrelationIdFormatter.DEFAULT.format(context::get);
assertThat(formatted).isEqualTo("[0123456789012345678901234567 -012345678901 ] ");
}
@Test
void formatWhenPartsAreLong() {
Map<String, String> context = new HashMap<>();
context.put("traceId", "01234567890123456789012345678901FFFF");
context.put("spanId", "0123456789012345FFFF");
String formatted = CorrelationIdFormatter.DEFAULT.format(context::get);
assertThat(formatted).isEqualTo("[01234567890123456789012345678901FFFF-0123456789012345FFFF] ");
}
@Test
void formatWithCustomSpec() {
Map<String, String> context = new HashMap<>();
context.put("a", "01234567890123456789012345678901");
context.put("b", "0123456789012345");
String formatted = CorrelationIdFormatter.of("a(32),b(16)").format(context::get);
assertThat(formatted).isEqualTo("[01234567890123456789012345678901-0123456789012345] ");
}
@Test
void formatToWithDefaultSpec() {
Map<String, String> context = new HashMap<>();
context.put("traceId", "01234567890123456789012345678901");
context.put("spanId", "0123456789012345");
StringBuilder formatted = new StringBuilder();
CorrelationIdFormatter.of("").formatTo(context::get, formatted);
assertThat(formatted).hasToString("[01234567890123456789012345678901-0123456789012345] ");
}
@Test
void ofWhenSpecIsMalformed() {
assertThatIllegalStateException().isThrownBy(() -> CorrelationIdFormatter.of("good(12),bad"))
.withMessage("Unable to parse correlation formatter spec 'good(12),bad'")
.havingCause()
.withMessage("Invalid specification part 'bad'");
}
@Test
void ofWhenSpecIsEmpty() {
assertThat(CorrelationIdFormatter.of("")).isSameAs(CorrelationIdFormatter.DEFAULT);
}
@Test
void toStringReturnsSpec() {
assertThat(CorrelationIdFormatter.DEFAULT).hasToString("traceId(32),spanId(16)");
assertThat(CorrelationIdFormatter.of("a(32),b(16)")).hasToString("a(32),b(16)");
}
}

@ -18,6 +18,7 @@ package org.springframework.boot.logging;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
@ -36,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Andy Wilkinson
* @author Eddú Meléndez
* @author Jonatan Ivanov
*/
class LoggingSystemPropertiesTests {
@ -115,6 +117,25 @@ class LoggingSystemPropertiesTests {
return System.getProperty(property.getEnvironmentVariableName());
}
@Test
void correlationPatternIsSet() {
new LoggingSystemProperties(
new MockEnvironment().withProperty("logging.pattern.correlation", "correlation pattern"))
.apply(null);
assertThat(System.getProperty(LoggingSystemProperty.CORRELATION_PATTERN.getEnvironmentVariableName()))
.isEqualTo("correlation pattern");
}
@Test
void defaultValueResolverIsUsed() {
MockEnvironment environment = new MockEnvironment();
Map<String, String> defaultValues = Map
.of(LoggingSystemProperty.CORRELATION_PATTERN.getApplicationPropertyName(), "default correlation pattern");
new LoggingSystemProperties(environment, defaultValues::get, null).apply(null);
assertThat(System.getProperty(LoggingSystemProperty.CORRELATION_PATTERN.getEnvironmentVariableName()))
.isEqualTo("default correlation pattern");
}
private Environment environment(String key, Object value) {
StandardEnvironment environment = new StandardEnvironment();
environment.getPropertySources().addLast(new MapPropertySource("test", Collections.singletonMap(key, value)));

@ -0,0 +1,65 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging.log4j2;
import java.util.Map;
import org.apache.logging.log4j.core.AbstractLogEvent;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CorrelationIdConverter}.
*
* @author Phillip Webb
*/
class CorrelationIdConverterTests {
private CorrelationIdConverter converter = CorrelationIdConverter.newInstance(null);
private final LogEvent event = new TestLogEvent();
@Test
void defaultPattern() {
StringBuilder result = new StringBuilder();
this.converter.format(this.event, result);
assertThat(result).hasToString("[01234567890123456789012345678901-0123456789012345] ");
}
@Test
void customPattern() {
this.converter = CorrelationIdConverter.newInstance(new String[] { "traceId(0),spanId(0)" });
StringBuilder result = new StringBuilder();
this.converter.format(this.event, result);
assertThat(result).hasToString("[01234567890123456789012345678901-0123456789012345] ");
}
static class TestLogEvent extends AbstractLogEvent {
@Override
public ReadOnlyStringMap getContextData() {
return new JdkMapAdapterStringMap(
Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
}
}
}

@ -39,6 +39,7 @@ import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.config.Reconfigurable;
import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
import org.apache.logging.log4j.jul.Log4jBridgeHandler;
import org.apache.logging.log4j.util.PropertiesUtil;
@ -47,12 +48,15 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.MDC;
import org.springframework.boot.logging.AbstractLoggingSystemTests;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemProperties;
import org.springframework.boot.logging.LoggingSystemProperty;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.boot.testsupport.logging.ConfigureClasspathToPreferLog4j2;
@ -86,12 +90,11 @@ import static org.mockito.Mockito.times;
@ConfigureClasspathToPreferLog4j2
class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {
private final TestLog4J2LoggingSystem loggingSystem = new TestLog4J2LoggingSystem();
private TestLog4J2LoggingSystem loggingSystem;
private final MockEnvironment environment = new MockEnvironment();
private MockEnvironment environment;
private final LoggingInitializationContext initializationContext = new LoggingInitializationContext(
this.environment);
private LoggingInitializationContext initializationContext;
private Logger logger;
@ -99,6 +102,10 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {
@BeforeEach
void setup() {
PluginRegistry.getInstance().clear();
this.loggingSystem = new TestLog4J2LoggingSystem();
this.environment = new MockEnvironment();
this.initializationContext = new LoggingInitializationContext(this.environment);
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
this.configuration = loggerContext.getConfiguration();
this.loggingSystem.cleanUp();
@ -113,6 +120,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {
loggerContext.stop();
loggerContext.start(((Reconfigurable) this.configuration).reconfigure());
cleanUpPropertySources();
PluginRegistry.getInstance().clear();
}
@SuppressWarnings("unchecked")
@ -495,6 +503,67 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {
.withMessageContaining("http has not been enabled");
}
@Test
void correlationLoggingToFileWhenExpectCorrelationIdTrueAndMdcContent() {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
new LoggingSystemProperties(this.environment).apply();
File file = new File(tmpDir(), "log4j2-test.log");
LogFile logFile = getLogFile(file.getPath(), null);
this.loggingSystem.setStandardConfigLocations(false);
this.loggingSystem.beforeInitialize();
this.loggingSystem.initialize(this.initializationContext, null, logFile);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(file, "Hello world"))
.contains(" [01234567890123456789012345678901-0123456789012345] ");
}
@Test
void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndMdcContent(CapturedOutput output) {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
this.loggingSystem.setStandardConfigLocations(false);
this.loggingSystem.beforeInitialize();
this.loggingSystem.initialize(this.initializationContext, null, null);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world"))
.contains(" [01234567890123456789012345678901-0123456789012345] ");
}
@Test
void correlationLoggingToConsoleWhenExpectCorrelationIdFalseAndMdcContent(CapturedOutput output) {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "false");
this.loggingSystem.setStandardConfigLocations(false);
this.loggingSystem.beforeInitialize();
this.loggingSystem.initialize(this.initializationContext, null, null);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world")).doesNotContain("0123456789012345");
}
@Test
void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndNoMdcContent(CapturedOutput output) {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
this.loggingSystem.setStandardConfigLocations(false);
this.loggingSystem.beforeInitialize();
this.loggingSystem.initialize(this.initializationContext, null, null);
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world"))
.contains(" [ ] ");
}
@Test
void correlationLoggingToConsoleWhenHasCorrelationPattern(CapturedOutput output) {
this.environment.setProperty("logging.pattern.correlation", "%correlationId{spanId(0),traceId(0)}");
this.loggingSystem.setStandardConfigLocations(false);
this.loggingSystem.beforeInitialize();
this.loggingSystem.initialize(this.initializationContext, null, null);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world"))
.contains(" [0123456789012345-01234567890123456789012345678901] ");
}
private String getRelativeClasspathLocation(String fileName) {
String defaultPath = ClassUtils.getPackageName(getClass());
defaultPath = defaultPath.replace('.', '/');

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2023 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.
@ -27,6 +27,8 @@ class TestLog4J2LoggingSystem extends Log4J2LoggingSystem {
private final List<String> availableClasses = new ArrayList<>();
private String[] standardConfigLocations;
TestLog4J2LoggingSystem() {
super(TestLog4J2LoggingSystem.class.getClassLoader());
}
@ -44,4 +46,18 @@ class TestLog4J2LoggingSystem extends Log4J2LoggingSystem {
Collections.addAll(this.availableClasses, classNames);
}
@Override
protected String[] getStandardConfigLocations() {
return (this.standardConfigLocations != null) ? this.standardConfigLocations
: super.getStandardConfigLocations();
}
void setStandardConfigLocations(boolean standardConfigLocations) {
this.standardConfigLocations = (!standardConfigLocations) ? new String[0] : null;
}
void setStandardConfigLocations(String[] standardConfigLocations) {
this.standardConfigLocations = standardConfigLocations;
}
}

@ -0,0 +1,67 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.logging.logback;
import java.util.List;
import java.util.Map;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.LoggingEvent;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CorrelationIdConverter}.
*
* @author Phillip Webb
*/
class CorrelationIdConverterTests {
private final CorrelationIdConverter converter;
private final LoggingEvent event = new LoggingEvent();
CorrelationIdConverterTests() {
this.converter = new CorrelationIdConverter();
this.converter.setContext(new LoggerContext());
}
@Test
void defaultPattern() {
addMdcProperties(this.event);
this.converter.start();
String converted = this.converter.convert(this.event);
this.converter.stop();
assertThat(converted).isEqualTo("[01234567890123456789012345678901-0123456789012345] ");
}
@Test
void customPattern() {
this.converter.setOptionList(List.of("traceId(0)", "spanId(0)"));
addMdcProperties(this.event);
this.converter.start();
String converted = this.converter.convert(this.event);
this.converter.stop();
assertThat(converted).isEqualTo("[01234567890123456789012345678901-0123456789012345] ");
}
private void addMdcProperties(LoggingEvent event) {
event.setMDCPropertyMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
}
}

@ -45,6 +45,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
@ -87,6 +88,7 @@ import static org.mockito.Mockito.times;
* @author Robert Thornton
* @author Eddú Meléndez
* @author Scott Frederick
* @author Jonatan Ivanov
*/
@ExtendWith(OutputCaptureExtension.class)
class LogbackLoggingSystemTests extends AbstractLoggingSystemTests {
@ -547,6 +549,7 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests {
(field) -> expectedProperties.add((String) field.get(null)), this::isPublicStaticFinal);
expectedProperties.removeAll(Arrays.asList("LOG_FILE", "LOG_PATH"));
expectedProperties.add("org.jboss.logging.provider");
expectedProperties.add("LOG_CORRELATION_PATTERN");
assertThat(properties).containsOnlyKeys(expectedProperties);
assertThat(properties).containsEntry("CONSOLE_LOG_CHARSET", Charset.defaultCharset().name());
}
@ -679,8 +682,81 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests {
assertThat(output).contains("<springProfile> elements cannot be nested within an");
}
@Test
void correlationLoggingToFileWhenExpectCorrelationIdTrueAndMdcContent() {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
File file = new File(tmpDir(), "logback-test.log");
LogFile logFile = getLogFile(file.getPath(), null);
initialize(this.initializationContext, null, logFile);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(file, "Hello world"))
.contains(" [01234567890123456789012345678901-0123456789012345] ");
}
@Test
void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndMdcContent(CapturedOutput output) {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
initialize(this.initializationContext, null, null);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world"))
.contains(" [01234567890123456789012345678901-0123456789012345] ");
}
@Test
void correlationLoggingToConsoleWhenExpectCorrelationIdFalseAndMdcContent(CapturedOutput output) {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "false");
initialize(this.initializationContext, null, null);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world")).doesNotContain("0123456789012345");
}
@Test
void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndNoMdcContent(CapturedOutput output) {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
initialize(this.initializationContext, null, null);
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world"))
.contains(" [ ] ");
}
@Test
void correlationLoggingToConsoleWhenHasCorrelationPattern(CapturedOutput output) {
this.environment.setProperty("logging.pattern.correlation", "%correlationId{spanId(0),traceId(0)}");
initialize(this.initializationContext, null, null);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world"))
.contains(" [0123456789012345-01234567890123456789012345678901] ");
}
@Test
void correlationLoggingToConsoleWhenUsingXmlConfiguration(CapturedOutput output) {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
initialize(this.initializationContext, "classpath:logback-include-base.xml", null);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(output, "Hello world"))
.contains(" [01234567890123456789012345678901-0123456789012345] ");
}
@Test
void correlationLoggingToConsoleWhenUsingFileConfiguration() {
this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
File file = new File(tmpDir(), "logback-test.log");
LogFile logFile = getLogFile(file.getPath(), null);
initialize(this.initializationContext, "classpath:logback-include-base.xml", logFile);
MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345"));
this.logger.info("Hello world");
assertThat(getLineWithText(file, "Hello world"))
.contains(" [01234567890123456789012345678901-0123456789012345] ");
}
private void initialize(LoggingInitializationContext context, String configLocation, LogFile logFile) {
this.loggingSystem.getSystemProperties((ConfigurableEnvironment) context.getEnvironment()).apply(logFile);
this.loggingSystem.beforeInitialize();
this.loggingSystem.initialize(context, configLocation, logFile);
}
@ -702,15 +778,4 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests {
return (SizeAndTimeBasedRollingPolicy<?>) getFileAppender().getRollingPolicy();
}
private String getLineWithText(File file, CharSequence outputSearch) {
return getLineWithText(contentOf(file), outputSearch);
}
private String getLineWithText(CharSequence output, CharSequence outputSearch) {
return Arrays.stream(output.toString().split("\\r?\\n"))
.filter((line) -> line.contains(outputSearch))
.findFirst()
.orElse(null);
}
}

Loading…
Cancel
Save