Merge pull request #51656 from o-shevchenko

Closes gh-51656

* gh-51656:
  Add baggage tag fields to spans with OpenTelemetry
This commit is contained in:
Andy Wilkinson
2026-09-11 11:21:32 +01:00
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 {