Add baggage tag fields to spans with OpenTelemetry

With OpenTelemetry, fields listed in management.tracing.baggage.tag-fields
only became span tags when the application itself touched the baggage
through the Tracer API. Baggage that arrived with a request was
propagated correctly, but spans were not tagged with it (see
micrometer-metrics/tracing#933).

This commit registers Micrometer Tracing's BaggageTaggingSpanProcessor
whenever baggage is enabled and at least one tag field is configured, so
that spans are tagged with the baggage that is present in their parent
context. A custom BaggageTaggingSpanProcessor bean backs off the
auto-configured one. The tag-fields property is now also mentioned in
the baggage section of the tracing documentation.

See gh-51656

Signed-off-by: Oleksandr Shevchenko <oleksandr.shevchenko@datarobot.com>
This commit is contained in:
Oleksandr Shevchenko
2026-09-11 11:18:02 +01:00
committed by Andy Wilkinson
parent 924d10733e
commit ce75f008b0
3 changed files with 91 additions and 0 deletions
@@ -236,6 +236,9 @@ Baggage is propagated wherever Micrometer Observation instruments the transport,
If you want to propagate the baggage to the MDC, use the configprop:management.tracing.baggage.correlation.fields[] configuration property.
For the example above, setting this property to `baggage1` results in an MDC entry named `baggage1`.
If you want baggage values to be added as tags on spans, use the configprop:management.tracing.baggage.tag-fields[] configuration property.
For the example above, setting this property to `baggage1` results in spans that are tagged with `baggage1=value1`.
[[actuator.micrometer-tracing.tests]]
@@ -18,18 +18,22 @@ package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure;
import java.util.List;
import io.micrometer.tracing.otel.bridge.BaggageTaggingSpanProcessor;
import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator;
import io.opentelemetry.context.propagation.TextMapPropagator;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.OnPropertyListCondition;
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.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
/**
@@ -37,6 +41,7 @@ import org.springframework.context.annotation.Configuration;
* {@link OpenTelemetryTracingAutoConfiguration}.
*
* @author Moritz Halbritter
* @author Oleksandr Shevchenko
*/
class OpenTelemetryPropagationConfigurations {
@@ -87,6 +92,26 @@ class OpenTelemetryPropagationConfigurations {
return new Slf4JBaggageEventListener(this.tracingProperties.getBaggage().getCorrelation().getFields());
}
@Bean
@ConditionalOnMissingBean
@Conditional(OnBaggageTagFieldsCondition.class)
BaggageTaggingSpanProcessor otelBaggageTaggingSpanProcessor() {
return new BaggageTaggingSpanProcessor(this.tracingProperties.getBaggage().getTagFields());
}
/**
* Condition that matches when {@code management.tracing.baggage.tag-fields} has
* at least one entry.
*/
static class OnBaggageTagFieldsCondition extends OnPropertyListCondition {
OnBaggageTagFieldsCondition() {
super("management.tracing.baggage.tag-fields",
() -> ConditionMessage.forCondition("Baggage tag fields"));
}
}
}
/**
@@ -30,6 +30,7 @@ import io.micrometer.tracing.SpanCustomizer;
import io.micrometer.tracing.Tracer.SpanInScope;
import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler;
import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler;
import io.micrometer.tracing.otel.bridge.BaggageTaggingSpanProcessor;
import io.micrometer.tracing.otel.bridge.EventListener;
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
import io.micrometer.tracing.otel.bridge.OtelPropagator;
@@ -40,18 +41,21 @@ import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
import io.micrometer.tracing.otel.bridge.Slf4JEventListener;
import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator;
import io.micrometer.tracing.propagation.Propagator;
import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Scope;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SpanLimits;
import io.opentelemetry.sdk.trace.SpanProcessor;
@@ -91,6 +95,7 @@ import static org.mockito.Mockito.mock;
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Yanming Zhou
* @author Oleksandr Shevchenko
*/
class OpenTelemetryTracingAutoConfigurationTests {
@@ -390,6 +395,54 @@ class OpenTelemetryTracingAutoConfigurationTests {
});
}
@Test
void shouldSupplyBaggageTaggingSpanProcessorWhenTagFieldsAreConfigured() {
this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=t1,t2").run((context) -> {
assertThat(context).hasSingleBean(BaggageTaggingSpanProcessor.class);
SpanProcessors spanProcessors = context.getBean(SpanProcessors.class);
assertThat(spanProcessors).anyMatch(BaggageTaggingSpanProcessor.class::isInstance);
});
}
@Test
void shouldNotSupplyBaggageTaggingSpanProcessorWhenTagFieldsAreNotConfigured() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(BaggageTaggingSpanProcessor.class));
}
@Test
void shouldNotSupplyBaggageTaggingSpanProcessorWhenBaggageDisabled() {
this.contextRunner
.withPropertyValues("management.tracing.baggage.enabled=false", "management.tracing.baggage.tag-fields=t1")
.run((context) -> assertThat(context).doesNotHaveBean(BaggageTaggingSpanProcessor.class));
}
@Test
void shouldBackOffOnCustomBaggageTaggingSpanProcessor() {
this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=t1")
.withUserConfiguration(CustomBaggageTaggingSpanProcessorConfiguration.class)
.run((context) -> {
assertThat(context).hasBean("customBaggageTaggingSpanProcessor");
assertThat(context).hasSingleBean(BaggageTaggingSpanProcessor.class);
});
}
@Test
void baggageTaggingSpanProcessorShouldTagSpansWithBaggageFromParentContext() {
this.contextRunner
.withPropertyValues("management.tracing.baggage.tag-fields=t1",
"management.tracing.sampling.probability=1.0")
.run((context) -> {
Tracer tracer = context.getBean(Tracer.class);
try (Scope scope = Baggage.builder().put("t1", "v1").put("other", "v2").build().makeCurrent()) {
Span span = tracer.spanBuilder("test").startSpan();
span.end();
assertThat(span).isInstanceOf(ReadableSpan.class);
assertThat(((ReadableSpan) span).getAttribute(AttributeKey.stringKey("t1"))).isEqualTo("v1");
assertThat(((ReadableSpan) span).getAttribute(AttributeKey.stringKey("other"))).isNull();
}
});
}
@Test
void spanLimitsShouldBeConfiguredWithCustomProperties() {
this.contextRunner
@@ -608,6 +661,16 @@ class OpenTelemetryTracingAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
private static final class CustomBaggageTaggingSpanProcessorConfiguration {
@Bean
BaggageTaggingSpanProcessor customBaggageTaggingSpanProcessor() {
return new BaggageTaggingSpanProcessor(List.of("custom"));
}
}
@Configuration(proxyBeanMethods = false)
private static final class AdditionalSpanProcessorConfiguration {