From ce75f008b02a853ffa370135a48095f5d603f3ed Mon Sep 17 00:00:00 2001 From: Oleksandr Shevchenko Date: Thu, 10 Sep 2026 15:53:24 +0300 Subject: [PATCH] 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 --- .../reference/pages/actuator/tracing.adoc | 3 + ...penTelemetryPropagationConfigurations.java | 25 ++++++++ ...elemetryTracingAutoConfigurationTests.java | 63 +++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc index e0cccc30931..6cdd91017dd 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/actuator/tracing.adoc @@ -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]] diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryPropagationConfigurations.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryPropagationConfigurations.java index 4f9fb4dbb01..540fc9053a2 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryPropagationConfigurations.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/main/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryPropagationConfigurations.java @@ -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")); + } + + } + } /** diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java index 9e86db47034..4c79a1b6ece 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/OpenTelemetryTracingAutoConfigurationTests.java @@ -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 {