Polish "Allow configuring Micrometer Tracing MDC keys"

Derive logging.pattern.correlation from the configured MDC keys so that
log correlation keeps working when the keys are customized, instead of
silently rendering a blank correlation field.

Reject empty MDC keys and only clear Brave's default correlation fields
when the keys have been customized, so that applications using the
defaults are unaffected if Brave adds a default field.

Replace the tests that asserted on bean wiring with integration tests
covering the MDC contents, plus a smoke test for the log output.

See gh-50595
This commit is contained in:
Moritz Halbritter
2026-08-25 13:54:00 +02:00
parent dc2274e5b6
commit 3f7cebd802
13 changed files with 296 additions and 105 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]
@@ -141,14 +141,12 @@ class BravePropagationConfigurations {
CorrelationScopeDecorator.Builder mdcCorrelationScopeDecoratorBuilder(
ObjectProvider<CorrelationScopeCustomizer> correlationScopeCustomizers) {
Mdc mdc = this.tracingProperties.getMdc();
CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder()
// Clear existing traceId/spanId backage field mappings
// so the MDC key names can be customized below.
// BravePropagationConfigurationsTests validates the assumption that
// the builder only configures the trace/span id by default.
.clear()
.add(SingleCorrelationField.newBuilder(BaggageFields.TRACE_ID).name(mdc.getTraceIdKey()).build())
.add(SingleCorrelationField.newBuilder(BaggageFields.SPAN_ID).name(mdc.getSpanIdKey()).build());
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,29 +260,18 @@ 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 correlationScopeDecoratorUsesDefaultMdcKeys() {
this.contextRunner.run((context) -> {
ScopeDecorator scopeDecorator = context.getBean(ScopeDecorator.class);
assertThat(scopeDecorator)
.extracting("fields", InstanceOfAssertFactories.array(SingleCorrelationField[].class))
.extracting(SingleCorrelationField::name)
.containsExactly("traceId", "spanId");
});
}
@Test
void correlationScopeDecoratorUsesCustomMdcKeys() {
this.contextRunner
.withPropertyValues("management.tracing.mdc.trace-id-key=customTraceId",
"management.tracing.mdc.span-id-key=customSpanId")
.run((context) -> {
ScopeDecorator scopeDecorator = context.getBean(ScopeDecorator.class);
assertThat(scopeDecorator)
.extracting("fields", InstanceOfAssertFactories.array(SingleCorrelationField[].class))
.extracting(SingleCorrelationField::name)
.containsExactly("customTraceId", "customSpanId");
});
void shouldOnlyHaveTraceIdAndSpanIdCorrelationFieldsByDefaultInBrave() {
assertThat(MDCScopeDecorator.newBuilder().configs())
.extracting((config) -> ((SingleCorrelationField) config).name())
.containsExactlyInAnyOrder(BaggageFields.TRACE_ID.name(), BaggageFields.SPAN_ID.name());
}
@Test
@@ -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) {
@@ -1,48 +0,0 @@
/*
* 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.brave.autoconfigure;
import brave.baggage.BaggageFields;
import brave.baggage.CorrelationScopeConfig.SingleCorrelationField;
import brave.baggage.CorrelationScopeDecorator;
import brave.context.slf4j.MDCScopeDecorator;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BravePropagationConfigurations}.
*/
class BravePropagationConfigurationsTests {
/**
* Validates the assumption that {@link MDCScopeDecorator#newBuilder()} only
* configures traceId and spanId by default. The
* {@link BravePropagationConfigurations.PropagationWithBaggage#mdcCorrelationScopeDecoratorBuilder}
* method clears the builder's defaults and re-adds only these two fields so their MDC
* key names can be customized. If Brave adds new default fields in the future, the
* {@code .clear()} call in that method would silently drop them, and this test will
* catch that.
*/
@Test
void mdcScopeDecoratorBuilderShouldOnlyHaveTraceIdAndSpanIdByDefault() {
CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder();
assertThat(builder.configs()).extracting((config) -> ((SingleCorrelationField) config).name())
.containsExactlyInAnyOrder(BaggageFields.TRACE_ID.name(), BaggageFields.SPAN_ID.name());
}
}
@@ -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) {
@@ -192,27 +192,6 @@ class OpenTelemetryTracingAutoConfigurationTests {
});
}
@Test
void slf4jEventListenerUsesDefaultMdcKeys() {
this.contextRunner.run((context) -> {
Slf4JEventListener listener = context.getBean(Slf4JEventListener.class);
assertThat(listener).hasFieldOrPropertyWithValue("traceIdKey", "traceId")
.hasFieldOrPropertyWithValue("spanIdKey", "spanId");
});
}
@Test
void slf4jEventListenerUsesCustomMdcKeys() {
this.contextRunner
.withPropertyValues("management.tracing.mdc.trace-id-key=customTraceId",
"management.tracing.mdc.span-id-key=customSpanId")
.run((context) -> {
Slf4JEventListener listener = context.getBean(Slf4JEventListener.class);
assertThat(listener).hasFieldOrPropertyWithValue("traceIdKey", "customTraceId")
.hasFieldOrPropertyWithValue("spanIdKey", "customSpanId");
});
}
@ParameterizedTest
@ValueSource(strings = { "io.micrometer.tracing.otel", "io.opentelemetry.sdk", "io.opentelemetry.api" })
void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) {
@@ -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.
@@ -272,21 +273,26 @@ public class TracingProperties {
*/
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 = "traceId";
private String traceIdKey = DEFAULT_TRACE_ID_KEY;
/**
* Key under which the span ID is added to the {@code org.slf4j.MDC}.
*/
private String spanIdKey = "spanId";
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;
}
@@ -295,9 +301,18 @@ public class TracingProperties {
}
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);
}
}
/**
@@ -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()));
}
}