Merge pull request #50595 from philsttr

Closes gh-50595

* pr/50595:
  Polish "Allow configuring Micrometer Tracing MDC keys"
  Allow configuring Micrometer Tracing MDC keys
This commit is contained in:
Moritz Halbritter
2026-08-25 13:54:32 +02:00
12 changed files with 344 additions and 5 deletions
@@ -75,7 +75,14 @@ If you are using Micrometer Tracing, Spring Boot will include correlation IDs in
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]`.
The `traceId` and `spanId` MDC key names can be customized using the configprop:management.tracing.mdc.trace-id-key[] and configprop:management.tracing.mdc.span-id-key[] properties.
The default correlation ID format follows those keys automatically, padding them to 32 and 16 characters respectively, so log correlation keeps working without further configuration.
NOTE: With Brave, the MDC key names are only applied when baggage is enabled.
Setting configprop:management.tracing.baggage.enabled[] to `false` stops Brave from writing the trace and span IDs to the MDC altogether, which disables log correlation.
If you prefer to use a different format for your correlation ID, you can use the configprop:logging.pattern.correlation[] property to define one.
A value that you set yourself takes precedence over the format derived from the MDC key names, so it has to reference any customized key names itself.
For example, the following will provide a correlation ID for Logback in format previously used by Spring Cloud Sleuth:
[configprops,yaml]
@@ -19,6 +19,7 @@ package org.springframework.boot.micrometer.tracing.brave.autoconfigure;
import java.util.List;
import brave.baggage.BaggageField;
import brave.baggage.BaggageFields;
import brave.baggage.BaggagePropagation;
import brave.baggage.BaggagePropagation.FactoryBuilder;
import brave.baggage.BaggagePropagationConfig;
@@ -40,6 +41,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.micrometer.tracing.autoconfigure.ConditionalOnEnabledTracingExport;
import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties;
import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Baggage.Correlation;
import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Mdc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@@ -138,7 +140,13 @@ class BravePropagationConfigurations {
@ConditionalOnMissingBean
CorrelationScopeDecorator.Builder mdcCorrelationScopeDecoratorBuilder(
ObjectProvider<CorrelationScopeCustomizer> correlationScopeCustomizers) {
Mdc mdc = this.tracingProperties.getMdc();
CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder();
if (mdc.isCustomized()) {
builder.clear()
.add(SingleCorrelationField.newBuilder(BaggageFields.TRACE_ID).name(mdc.getTraceIdKey()).build())
.add(SingleCorrelationField.newBuilder(BaggageFields.SPAN_ID).name(mdc.getSpanIdKey()).build());
}
correlationScopeCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
@@ -26,8 +26,10 @@ import brave.Span;
import brave.SpanCustomizer;
import brave.Tracer;
import brave.Tracing;
import brave.baggage.BaggageFields;
import brave.baggage.BaggagePropagation;
import brave.baggage.CorrelationScopeConfig.SingleCorrelationField;
import brave.context.slf4j.MDCScopeDecorator;
import brave.handler.SpanHandler;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContext.ScopeDecorator;
@@ -258,6 +260,20 @@ class BraveAutoConfigurationTests {
.run((context) -> assertThat(context).hasBean("mdcCorrelationScopeDecoratorBuilder"));
}
/**
* Guards the assumption behind the {@code clear()} call that
* {@code BravePropagationConfigurations} makes when the MDC keys have been
* customized: if Brave ever adds another default correlation field, this fails
* instead of that field silently disappearing from the MDC of applications using
* custom keys.
*/
@Test
void shouldOnlyHaveTraceIdAndSpanIdCorrelationFieldsByDefaultInBrave() {
assertThat(MDCScopeDecorator.newBuilder().configs())
.extracting((config) -> ((SingleCorrelationField) config).name())
.containsExactlyInAnyOrder(BaggageFields.TRACE_ID.name(), BaggageFields.SPAN_ID.name());
}
@Test
void shouldHave128BitTraceId() {
this.contextRunner.run((context) -> {
@@ -24,6 +24,7 @@ import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.slf4j.MDC;
@@ -79,6 +80,28 @@ class BraveBaggagePropagationIntegrationTests {
});
}
@Test
void shouldUseCustomMdcKeysForTraceAndSpanId() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
.withPropertyValues("management.tracing.mdc.trace-id-key=customTraceId",
"management.tracing.mdc.span-id-key=customSpanId")
.run((context) -> {
Tracer tracer = tracer(context);
Span span = createSpan(tracer);
try (Tracer.SpanInScope scope = tracer.withSpan(span.start())) {
assertMdcValue("customTraceId", span.context().traceId());
assertMdcValue("customSpanId", span.context().spanId());
assertUnsetMdc("traceId");
assertUnsetMdc("spanId");
}
finally {
span.end();
}
assertUnsetMdc("customTraceId");
assertUnsetMdc("customSpanId");
});
}
@ParameterizedTest
@EnumSource
void shouldRemoveEntriesFromMdcForNullSpan(AutoConfig autoConfig) {
@@ -59,6 +59,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.micrometer.tracing.autoconfigure.MicrometerTracingAutoConfiguration;
import org.springframework.boot.micrometer.tracing.autoconfigure.NoopTracerAutoConfiguration;
import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties;
import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Mdc;
import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.NoPropagation;
import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.PropagationWithBaggage;
import org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage;
@@ -219,7 +220,8 @@ public final class OpenTelemetryTracingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
Slf4JEventListener otelSlf4JEventListener() {
return new Slf4JEventListener();
Mdc mdc = this.tracingProperties.getMdc();
return new Slf4JEventListener(mdc.getTraceIdKey(), mdc.getSpanIdKey());
}
@Bean
@@ -26,6 +26,7 @@ import io.opentelemetry.context.Context;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.slf4j.MDC;
@@ -85,6 +86,30 @@ class OpenTelemetryBaggagePropagationIntegrationTests {
});
}
@Test
void shouldUseCustomMdcKeysForTraceAndSpanId() {
new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.mdc.trace-id-key=customTraceId",
"management.tracing.mdc.span-id-key=customSpanId")
.run((context) -> {
Tracer tracer = tracer(context);
Span span = createSpan(tracer);
try (Tracer.SpanInScope scope = tracer.withSpan(span.start())) {
assertMdcValue("customTraceId", span.context().traceId());
assertMdcValue("customSpanId", span.context().spanId());
assertUnsetMdc("traceId");
assertUnsetMdc("spanId");
}
finally {
span.end();
}
assertUnsetMdc("customTraceId");
assertUnsetMdc("customSpanId");
});
}
@ParameterizedTest
@EnumSource
void shouldRemoveEntriesFromMdcForNullSpan(AutoConfig autoConfig) {
@@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Mdc;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
@@ -31,10 +32,13 @@ import org.springframework.util.ClassUtils;
* {@link EnvironmentPostProcessor} to add a {@link PropertySource} to support log
* correlation IDs when Micrometer Tracing is present. Adds support for the
* {@value LoggingSystem#EXPECT_CORRELATION_ID_PROPERTY} property by delegating to
* {@code management.tracing.export.enabled}.
* {@code management.tracing.export.enabled}, and defaults
* {@code logging.pattern.correlation} to the MDC keys configured through
* {@code management.tracing.mdc.*}.
*
* @author Jonatan Ivanov
* @author Phillip Webb
* @author Moritz Halbritter
*/
class LogCorrelationEnvironmentPostProcessor implements EnvironmentPostProcessor {
@@ -52,6 +56,18 @@ class LogCorrelationEnvironmentPostProcessor implements EnvironmentPostProcessor
private static final String NAME = "logCorrelation";
private static final String CORRELATION_PATTERN_PROPERTY = "logging.pattern.correlation";
/**
* Expected trace ID length, matching {@code CorrelationIdFormatter.DEFAULT}.
*/
private static final int TRACE_ID_LENGTH = 32;
/**
* Expected span ID length, matching {@code CorrelationIdFormatter.DEFAULT}.
*/
private static final int SPAN_ID_LENGTH = 16;
private final Environment environment;
LogCorrelationPropertySource(Object source, Environment environment) {
@@ -61,17 +77,35 @@ class LogCorrelationEnvironmentPostProcessor implements EnvironmentPostProcessor
@Override
public String[] getPropertyNames() {
return new String[] { LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY };
return new String[] { LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, CORRELATION_PATTERN_PROPERTY };
}
@Override
public @Nullable Object getProperty(String name) {
if (name.equals(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY)) {
return this.environment.getProperty("management.tracing.export.enabled", Boolean.class, Boolean.TRUE);
return isExpectCorrelationId();
}
if (name.equals(CORRELATION_PATTERN_PROPERTY)) {
return getCorrelationPattern();
}
return null;
}
private Boolean isExpectCorrelationId() {
return this.environment.getProperty("management.tracing.export.enabled", Boolean.class, Boolean.TRUE);
}
private @Nullable String getCorrelationPattern() {
if (!isExpectCorrelationId()) {
return null;
}
String traceIdKey = this.environment.getProperty("management.tracing.mdc.trace-id-key",
Mdc.DEFAULT_TRACE_ID_KEY);
String spanIdKey = this.environment.getProperty("management.tracing.mdc.span-id-key",
Mdc.DEFAULT_SPAN_ID_KEY);
return "%%correlationId{%s(%d),%s(%d)}".formatted(traceIdKey, TRACE_ID_LENGTH, spanIdKey, SPAN_ID_LENGTH);
}
}
}
@@ -22,6 +22,7 @@ import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;
/**
* Configuration properties for tracing.
@@ -53,6 +54,11 @@ public class TracingProperties {
*/
private final Exemplars exemplars = new Exemplars();
/**
* {@code org.slf4j.MDC} key configuration for trace and span IDs.
*/
private final Mdc mdc = new Mdc();
public Sampling getSampling() {
return this.sampling;
}
@@ -69,6 +75,10 @@ public class TracingProperties {
return this.exemplars;
}
public Mdc getMdc() {
return this.mdc;
}
public static class Sampling {
/**
@@ -258,6 +268,53 @@ public class TracingProperties {
}
/**
* {@code org.slf4j.MDC} key configuration for trace and span IDs.
*/
public static class Mdc {
static final String DEFAULT_TRACE_ID_KEY = "traceId";
static final String DEFAULT_SPAN_ID_KEY = "spanId";
/**
* Key under which the trace ID is added to the {@code org.slf4j.MDC}.
*/
private String traceIdKey = DEFAULT_TRACE_ID_KEY;
/**
* Key under which the span ID is added to the {@code org.slf4j.MDC}.
*/
private String spanIdKey = DEFAULT_SPAN_ID_KEY;
public String getTraceIdKey() {
return this.traceIdKey;
}
public void setTraceIdKey(String traceIdKey) {
Assert.hasText(traceIdKey, "'traceIdKey' must not be empty");
this.traceIdKey = traceIdKey;
}
public String getSpanIdKey() {
return this.spanIdKey;
}
public void setSpanIdKey(String spanIdKey) {
Assert.hasText(spanIdKey, "'spanIdKey' must not be empty");
this.spanIdKey = spanIdKey;
}
/**
* Return whether any of the keys differs from its default.
* @return whether the keys have been customized
*/
public boolean isCustomized() {
return !DEFAULT_TRACE_ID_KEY.equals(this.traceIdKey) || !DEFAULT_SPAN_ID_KEY.equals(this.spanIdKey);
}
}
/**
* Exemplars configuration.
*/
@@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Jonatan Ivanov
* @author Phillip Webb
* @author Moritz Halbritter
*/
class LogCorrelationEnvironmentPostProcessorTests {
@@ -72,7 +73,42 @@ class LogCorrelationEnvironmentPostProcessorTests {
PropertySource<?> propertySource = this.environment.getPropertySources().get("logCorrelation");
assertThat(propertySource).isInstanceOf(EnumerablePropertySource.class);
assertThat(((EnumerablePropertySource<?>) propertySource).getPropertyNames())
.containsExactly(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY);
.containsExactly(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "logging.pattern.correlation");
}
@Test
void getCorrelationPatternWhenMdcKeysAreDefaultReturnsPatternUsingDefaultKeys() {
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty("logging.pattern.correlation"))
.isEqualTo("%correlationId{traceId(32),spanId(16)}");
}
@Test
void getCorrelationPatternWhenMdcKeysAreCustomizedReturnsPatternUsingThoseKeys() {
TestPropertyValues
.of("management.tracing.mdc.trace-id-key=customTraceId", "management.tracing.mdc.span-id-key=customSpanId")
.applyTo(this.environment);
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty("logging.pattern.correlation"))
.isEqualTo("%correlationId{customTraceId(32),customSpanId(16)}");
}
@Test
void getCorrelationPatternWhenSetByUserDoesNotOverride() {
TestPropertyValues
.of("management.tracing.mdc.trace-id-key=customTraceId", "logging.pattern.correlation=%correlationId{x(1)}")
.applyTo(this.environment);
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty("logging.pattern.correlation")).isEqualTo("%correlationId{x(1)}");
}
@Test
void getCorrelationPatternWhenTracingDisabledReturnsNull() {
TestPropertyValues
.of("management.tracing.export.enabled=false", "management.tracing.mdc.trace-id-key=customTraceId")
.applyTo(this.environment);
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty("logging.pattern.correlation")).isNull();
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2012-present 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.micrometer.tracing.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.micrometer.tracing.autoconfigure.TracingProperties.Mdc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link TracingProperties}.
*
* @author Moritz Halbritter
*/
class TracingPropertiesTests {
private final Mdc mdc = new TracingProperties().getMdc();
@Test
void shouldUseTraceIdAndSpanIdAsDefaultMdcKeys() {
assertThat(this.mdc.getTraceIdKey()).isEqualTo("traceId");
assertThat(this.mdc.getSpanIdKey()).isEqualTo("spanId");
}
@Test
void shouldNotBeCustomizedWhenMdcKeysAreDefault() {
assertThat(this.mdc.isCustomized()).isFalse();
}
@Test
void shouldBeCustomizedWhenAnMdcKeyDiffersFromItsDefault() {
this.mdc.setSpanIdKey("customSpanId");
assertThat(this.mdc.isCustomized()).isTrue();
}
@Test
void shouldRejectBlankTraceIdKey() {
assertThatIllegalArgumentException().isThrownBy(() -> this.mdc.setTraceIdKey(" "));
}
@Test
void shouldRejectBlankSpanIdKey() {
assertThatIllegalArgumentException().isThrownBy(() -> this.mdc.setSpanIdKey(" "));
}
}
@@ -26,6 +26,7 @@ dependencies {
implementation(project(":starter:spring-boot-starter-opentelemetry"))
testImplementation(project(":starter:spring-boot-starter-test"))
testImplementation(project(":test-support:spring-boot-test-support"))
testImplementation("io.micrometer:micrometer-registry-otlp")
dockerTestImplementation(project(":core:spring-boot-testcontainers"))
@@ -0,0 +1,68 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.opentelemetry;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests that the correlation ID in the log output follows the MDC keys configured through
* {@code management.tracing.mdc.*}.
* <p>
* Runs with a forked class path because {@code LOG_CORRELATION_PATTERN} is a JVM-wide
* system property that is only ever written once, so a context started by another test
* class would pin it to the default pattern.
*
* @author Moritz Halbritter
*/
@SpringBootTest(properties = { "management.tracing.mdc.trace-id-key=customTraceId",
"management.tracing.mdc.span-id-key=customSpanId" })
@ExtendWith(OutputCaptureExtension.class)
@ForkedClassPath
class LogCorrelationTests {
private static final Log logger = LogFactory.getLog(LogCorrelationTests.class);
@Autowired
private Tracer tracer;
@Test
void shouldUseCustomMdcKeysForCorrelationId(CapturedOutput output) {
Span span = this.tracer.nextSpan().name("test");
try (Tracer.SpanInScope scope = this.tracer.withSpan(span.start())) {
logger.info("Hello from a span");
}
finally {
span.end();
}
assertThat(output).containsPattern(
"\\[%s-%s\\].*Hello from a span".formatted(span.context().traceId(), span.context().spanId()));
}
}