diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java index 92c1269a74..ee702d6d1a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java @@ -22,6 +22,14 @@ import brave.Tracer; import brave.Tracing; import brave.Tracing.Builder; import brave.TracingCustomizer; +import brave.baggage.BaggageField; +import brave.baggage.BaggagePropagation; +import brave.baggage.BaggagePropagationConfig; +import brave.baggage.BaggagePropagationCustomizer; +import brave.baggage.CorrelationScopeConfig; +import brave.baggage.CorrelationScopeCustomizer; +import brave.baggage.CorrelationScopeDecorator; +import brave.context.slf4j.MDCScopeDecorator; import brave.handler.SpanHandler; import brave.http.HttpClientHandler; import brave.http.HttpClientRequest; @@ -34,6 +42,7 @@ import brave.propagation.B3Propagation; import brave.propagation.CurrentTraceContext; import brave.propagation.CurrentTraceContext.ScopeDecorator; import brave.propagation.CurrentTraceContextCustomizer; +import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; import brave.propagation.ThreadLocalCurrentTraceContext; import brave.sampler.Sampler; @@ -41,20 +50,28 @@ import io.micrometer.tracing.brave.bridge.BraveBaggageManager; import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext; import io.micrometer.tracing.brave.bridge.BraveHttpClientHandler; import io.micrometer.tracing.brave.bridge.BraveHttpServerHandler; +import io.micrometer.tracing.brave.bridge.BravePropagator; import io.micrometer.tracing.brave.bridge.BraveTracer; +import io.micrometer.tracing.brave.bridge.W3CPropagation; +import org.slf4j.MDC; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; /** * {@link EnableAutoConfiguration Auto-configuration} for Brave. * * @author Moritz Halbritter + * @author Marcin Grzejszczak * @since 3.0.0 */ @AutoConfiguration(before = MicrometerTracingAutoConfiguration.class) @@ -63,6 +80,8 @@ import org.springframework.core.env.Environment; @ConditionalOnEnabledTracing public class BraveAutoConfiguration { + private static final BraveBaggageManager BRAVE_BAGGAGE_MANAGER = new BraveBaggageManager(); + /** * Default value for application name if {@code spring.application.name} is not set. */ @@ -105,12 +124,6 @@ public class BraveAutoConfiguration { return builder.build(); } - @Bean - @ConditionalOnMissingBean - public Factory bravePropagationFactory() { - return B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE_NO_PARENT).build(); - } - @Bean @ConditionalOnMissingBean public Sampler braveSampler(TracingProperties properties) { @@ -137,15 +150,14 @@ public class BraveAutoConfiguration { @Bean @ConditionalOnMissingBean - BraveTracer braveTracerBridge(brave.Tracer tracer, CurrentTraceContext currentTraceContext, - BraveBaggageManager braveBaggageManager) { - return new BraveTracer(tracer, new BraveCurrentTraceContext(currentTraceContext), braveBaggageManager); + BraveTracer braveTracerBridge(brave.Tracer tracer, CurrentTraceContext currentTraceContext) { + return new BraveTracer(tracer, new BraveCurrentTraceContext(currentTraceContext), BRAVE_BAGGAGE_MANAGER); } @Bean @ConditionalOnMissingBean - BraveBaggageManager braveBaggageManager() { - return new BraveBaggageManager(); + BravePropagator bravePropagator(Tracing tracing) { + return new BravePropagator(tracing); } @Bean @@ -162,4 +174,108 @@ public class BraveAutoConfiguration { return new BraveHttpClientHandler(httpClientHandler); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.tracing.baggage.enabled", havingValue = "false", matchIfMissing = true) + static class BraveNoBaggageConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "W3C", + matchIfMissing = true) + Factory w3cPropagationNoBaggageFactory() { + return new W3CPropagation(BRAVE_BAGGAGE_MANAGER, List.of()); // TODO: Use + // snapshots + // of + // tracing + // to not + // use + // baggage + // for W3C + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "B3") + Factory b3PropagationNoBaggageFactory() { + return B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE_NO_PARENT).build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.tracing.baggage.enabled", matchIfMissing = true) + static class BraveBaggageConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "W3C", + matchIfMissing = true) + BaggagePropagation.FactoryBuilder w3cPropagationFactory() { + return BaggagePropagation.newFactoryBuilder(new W3CPropagation(BRAVE_BAGGAGE_MANAGER, List.of())); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "B3") + BaggagePropagation.FactoryBuilder b3PropagationFactory() { + return BaggagePropagation.newFactoryBuilder( + B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE_NO_PARENT).build()); + } + + @Bean + @ConditionalOnMissingBean + Propagation.Factory micrometerTracingPropagationWithBaggage(BaggagePropagation.FactoryBuilder factoryBuilder, + TracingProperties tracingProperties, + ObjectProvider> baggagePropagationCustomizers) { + List remoteFields = tracingProperties.getBaggage().getRemoteFields(); + for (String fieldName : remoteFields) { + factoryBuilder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create(fieldName))); + } + baggagePropagationCustomizers.ifAvailable( + (customizers) -> customizers.forEach((customizer) -> customizer.customize(factoryBuilder))); + return factoryBuilder.build(); + } + + @Bean + @ConditionalOnMissingBean(CorrelationScopeDecorator.class) + @ConditionalOnBean(CorrelationScopeDecorator.Builder.class) + @ConditionalOnProperty(value = "management.tracing.baggage.correlation.enabled", matchIfMissing = true) + ScopeDecorator correlationFieldsCorrelationScopeDecorator(TracingProperties properties, + ObjectProvider> correlationScopeCustomizers, + CorrelationScopeDecorator.Builder builder) { + List correlationFields = properties.getBaggage().getCorrelation().getFields(); + for (String field : correlationFields) { + builder.add(CorrelationScopeConfig.SingleCorrelationField.newBuilder(BaggageField.create(field)) + .flushOnUpdate().build()); + } + correlationScopeCustomizers + .ifAvailable((customizers) -> customizers.forEach((customizer) -> customizer.customize(builder))); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean(CorrelationScopeDecorator.class) + @ConditionalOnBean(CorrelationScopeDecorator.Builder.class) + @ConditionalOnProperty(value = "management.tracing.baggage.correlation.enabled", havingValue = "false") + ScopeDecorator noCorrelationFieldsCorrelationScopeDecorator(CorrelationScopeDecorator.Builder builder, + ObjectProvider> correlationScopeCustomizers) { + correlationScopeCustomizers + .ifAvailable((customizers) -> customizers.forEach((customizer) -> customizer.customize(builder))); + return builder.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CorrelationScopeDecoratorConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(MDC.class) + CorrelationScopeDecorator.Builder mdcCorrelationScopeDecoratorBuilder() { + return MDCScopeDecorator.newBuilder(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index 74bc43a06a..55a17e86b9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -23,17 +23,27 @@ import java.util.stream.Collectors; import io.micrometer.tracing.SamplerFunction; import io.micrometer.tracing.otel.bridge.DefaultHttpClientAttributesGetter; import io.micrometer.tracing.otel.bridge.DefaultHttpServerAttributesExtractor; +import io.micrometer.tracing.otel.bridge.EventListener; +import io.micrometer.tracing.otel.bridge.EventPublishingContextWrapper; import io.micrometer.tracing.otel.bridge.OtelBaggageManager; import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; import io.micrometer.tracing.otel.bridge.OtelHttpClientHandler; import io.micrometer.tracing.otel.bridge.OtelHttpServerHandler; +import io.micrometer.tracing.otel.bridge.OtelPropagator; import io.micrometer.tracing.otel.bridge.OtelTracer; import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher; +import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener; +import io.micrometer.tracing.otel.bridge.Slf4JEventListener; +import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -43,14 +53,17 @@ import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.slf4j.MDC; import org.springframework.boot.SpringBootVersion; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; /** @@ -103,7 +116,6 @@ public class OpenTelemetryAutoConfiguration { } @Bean - @ConditionalOnMissingBean SpanProcessor otelSpanProcessor(List spanExporter) { return SpanProcessor.composite(spanExporter.stream() .map((exporter) -> BatchSpanProcessor.builder(exporter).build()).collect(Collectors.toList())); @@ -118,21 +130,27 @@ public class OpenTelemetryAutoConfiguration { @Bean @ConditionalOnMissingBean OtelTracer micrometerOtelTracer(Tracer tracer, EventPublisher eventPublisher, - OtelCurrentTraceContext otelCurrentTraceContext) { + OtelCurrentTraceContext otelCurrentTraceContext, TracingProperties properties) { return new OtelTracer(tracer, otelCurrentTraceContext, eventPublisher, - new OtelBaggageManager(otelCurrentTraceContext, List.of(), List.of())); + new OtelBaggageManager(otelCurrentTraceContext, properties.getBaggage().getRemoteFields(), List.of())); } @Bean @ConditionalOnMissingBean - EventPublisher otelTracerEventPublisher() { - return (event) -> { - }; + OtelPropagator otelPropagator(ContextPropagators contextPropagators, Tracer tracer) { + return new OtelPropagator(contextPropagators, tracer); } @Bean @ConditionalOnMissingBean - OtelCurrentTraceContext otelCurrentTraceContext() { + EventPublisher otelTracerEventPublisher(List eventListeners) { + return new OTelEventPublisher(eventListeners); + } + + @Bean + @ConditionalOnMissingBean + OtelCurrentTraceContext otelCurrentTraceContext(EventPublisher publisher) { + ContextStorage.addWrapper(new EventPublishingContextWrapper(publisher)); return new OtelCurrentTraceContext(); } @@ -150,4 +168,112 @@ public class OpenTelemetryAutoConfiguration { new DefaultHttpServerAttributesExtractor()); } + @Configuration(proxyBeanMethods = false) + static class PropagationConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(B3Propagator.class) + static class B3NoBaggagePropagatorConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "B3") + B3Propagator b3TextMapPropagator() { + return B3Propagator.injectingSingleHeader(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.tracing.baggage.enabled", havingValue = "false") + static class W3CNoBaggagePropagatorConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "W3C", + matchIfMissing = true) + W3CTraceContextPropagator w3cTextMapPropagatorWithoutBaggage() { + return W3CTraceContextPropagator.getInstance(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.tracing.baggage.enabled", matchIfMissing = true) + static class W3CBaggagePropagatorConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "W3C", + matchIfMissing = true) + TextMapPropagator w3cTextMapPropagatorWithBaggage() { + return TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), + W3CBaggagePropagator.getInstance()); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class MicrometerTracingPropagationConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.tracing.baggage.enabled", matchIfMissing = true) + static class BaggagePropagatorConfiguration { + + @Bean + @ConditionalOnProperty(value = "management.tracing.propagation.type", havingValue = "B3") + BaggageTextMapPropagator b3BaggageTextMapPropagator(TracingProperties properties, + OtelCurrentTraceContext otelCurrentTraceContext) { + return new BaggageTextMapPropagator(properties.getBaggage().getRemoteFields(), new OtelBaggageManager( + otelCurrentTraceContext, properties.getBaggage().getRemoteFields(), List.of())); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(MDC.class) + static class Slf4jConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "management.tracing.baggage.correlation.enabled", matchIfMissing = true) + Slf4JBaggageEventListener otelSlf4JBaggageEventListener(TracingProperties tracingProperties) { + return new Slf4JBaggageEventListener(tracingProperties.getBaggage().getCorrelation().getFields()); + } + + } + + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(MDC.class) + static class Slf4jConfiguration { + + @Bean + @ConditionalOnMissingBean + Slf4JEventListener otelSlf4JEventListener() { + return new Slf4JEventListener(); + } + + } + + static class OTelEventPublisher implements EventPublisher { + + private final List listeners; + + OTelEventPublisher(List listeners) { + this.listeners = listeners; + } + + @Override + public void publishEvent(Object event) { + for (EventListener listener : this.listeners) { + listener.onEvent(event); + } + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java index 56ce52a93a..51a801045e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.tracing; +import java.util.ArrayList; +import java.util.List; + import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -32,10 +35,28 @@ public class TracingProperties { */ private final Sampling sampling = new Sampling(); + /** + * Baggage configuration. + */ + private final Baggage baggage = new Baggage(); + + /** + * Propagation configuration. + */ + private final Propagation propagation = new Propagation(); + public Sampling getSampling() { return this.sampling; } + public Baggage getBaggage() { + return this.baggage; + } + + public Propagation getPropagation() { + return this.propagation; + } + public static class Sampling { /** @@ -53,4 +74,111 @@ public class TracingProperties { } + public static class Baggage { + + /** + * Whether to enable Micrometer Tracing baggage propagation. + */ + private boolean enabled; + + /** + * Correlation configuration. + */ + private Correlation correlation = new Correlation(); + + /** + * List of fields that are referenced the same in-process as it is on the wire. + * For example, the field "x-vcap-request-id" would be set as-is including the + * prefix. + */ + private List remoteFields = new ArrayList<>(); + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Correlation getCorrelation() { + return this.correlation; + } + + public void setCorrelation(Correlation correlation) { + this.correlation = correlation; + } + + public List getRemoteFields() { + return this.remoteFields; + } + + public void setRemoteFields(List remoteFields) { + this.remoteFields = remoteFields; + } + + public static class Correlation { + + /** + * Whether to enable correlation of the baggage context with logging contexts. + */ + private boolean enabled = true; + + /** + * List of fields that should be correlated with the logging context. That + * means that these fields would end up as key-value pairs in e.g. MDC. + */ + private List fields = new ArrayList<>(); + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getFields() { + return this.fields; + } + + public void setFields(List fields) { + this.fields = fields; + } + + } + + } + + public static class Propagation { + + /** + * Tracing context propagation types. + */ + private PropagationType type = PropagationType.W3C; + + public PropagationType getType() { + return this.type; + } + + public void setType(PropagationType type) { + this.type = type; + } + + enum PropagationType { + + /** + * B3 propagation type. + */ + B3, + + /** + * W3C propagation type. + */ + W3C + + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index dfe63ad4b0..3af7c2ab55 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2086,6 +2086,10 @@ "replacement": "management.trace.http.include", "level": "error" } + }, + { + "name": "management.tracing.propagation.type", + "defaultValue": "W3C" } ], "hints": [ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageAutoConfigurationTests.java new file mode 100644 index 0000000000..f104fad7d5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageAutoConfigurationTests.java @@ -0,0 +1,182 @@ +/* + * Copyright 2012-2022 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.actuate.autoconfigure.tracing; + +import java.util.function.Supplier; + +import io.micrometer.tracing.BaggageInScope; +import io.micrometer.tracing.BaggageManager; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.opentelemetry.context.Context; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.slf4j.MDC; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for Baggage configuration. + * + * @author Marcin Grzejszczak + */ +class BaggageAutoConfigurationTests { + + static final String COUNTRY_CODE = "country-code"; + static final String BUSINESS_PROCESS = "bp"; + + @BeforeEach + @AfterEach + void setup() { + MDC.clear(); + } + + @ParameterizedTest + @EnumSource(AutoConfig.class) + void shouldSetEntriesToMdcFromSpanWithBaggage(AutoConfig autoConfig) { + autoConfig.get().run((context) -> { + Tracer tracer = tracer(context); + Span span = createSpan(tracer); + assertThatTracingContextIsInitialized(autoConfig); + try (Tracer.SpanInScope scope = tracer.withSpan(span.start()); + BaggageInScope fo = context.getBean(BaggageManager.class).createBaggage(COUNTRY_CODE) + .set(span.context(), "FO"); + BaggageInScope bp = context.getBean(BaggageManager.class).createBaggage(BUSINESS_PROCESS) + .set(span.context(), "ALM")) { + assertThat(MDC.get("traceId")).isEqualTo(span.context().traceId()); + assertThat(MDC.get(COUNTRY_CODE)).isEqualTo("FO"); + assertThat(MDC.get(BUSINESS_PROCESS)).isEqualTo("ALM"); + } + finally { + span.end(); + } + + assertThatMdcContainsUnsetTraceId(); + assertThat(MDC.get(COUNTRY_CODE)).isNull(); + assertThat(MDC.get(BUSINESS_PROCESS)).isNull(); + }); + } + + @ParameterizedTest + @EnumSource(AutoConfig.class) + void shouldRemoveEntriesFromMdcForNullSpan(AutoConfig autoConfig) { + autoConfig.get().run((context) -> { + Tracer tracer = tracer(context); + Span span = createSpan(tracer); + assertThatTracingContextIsInitialized(autoConfig); + try (Tracer.SpanInScope scope = tracer.withSpan(span.start()); + BaggageInScope fo = context.getBean(BaggageManager.class).createBaggage(COUNTRY_CODE) + .set(span.context(), "FO")) { + assertThat(MDC.get("traceId")).isEqualTo(span.context().traceId()); + assertThat(MDC.get(COUNTRY_CODE)).isEqualTo("FO"); + + try (Tracer.SpanInScope scope2 = tracer.withSpan(null)) { + assertThatMdcContainsUnsetTraceId(); + assertThat(MDC.get(COUNTRY_CODE)).isNullOrEmpty(); + } + + assertThat(MDC.get("traceId")).isEqualTo(span.context().traceId()); + assertThat(MDC.get(COUNTRY_CODE)).isEqualTo("FO"); + } + finally { + span.end(); + } + assertThatMdcContainsUnsetTraceId(); + assertThat(MDC.get(COUNTRY_CODE)).isNullOrEmpty(); + }); + } + + private Span createSpan(Tracer tracer) { + return tracer.nextSpan().name("span"); + } + + private Tracer tracer(ApplicationContext context) { + return context.getBean(Tracer.class); + } + + private void assertThatTracingContextIsInitialized(AutoConfig autoConfig) { + if (autoConfig == AutoConfig.OTEL_B3) { + assertThat(Context.current()).isEqualTo(Context.root()); + } + } + + private void assertThatMdcContainsUnsetTraceId() { + assertThat(isInvalidBraveTraceId() || isInvalidOtelTraceId()).isTrue(); + } + + private boolean isInvalidBraveTraceId() { + return MDC.get("traceId") == null; + } + + private boolean isInvalidOtelTraceId() { + return MDC.get("traceId").equals("00000000000000000000000000000000"); + } + + enum AutoConfig implements Supplier { + + BRAVE_W3C { + @Override + public ApplicationContextRunner get() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class)).withPropertyValues( + "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", + "management.tracing.baggage.correlation.fields=country-code,bp"); + } + }, + + OTEL_W3C { + @Override + public ApplicationContextRunner get() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + .withPropertyValues( + "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", + "management.tracing.baggage.correlation.fields=country-code,bp"); + } + }, + + BRAVE_B3 { + @Override + public ApplicationContextRunner get() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class)) + .withPropertyValues("management.tracing.propagation.type=B3", + "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", + "management.tracing.baggage.correlation.fields=country-code,bp"); + } + }, + + OTEL_B3 { + @Override + public ApplicationContextRunner get() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + .withPropertyValues("management.tracing.propagation.type=B3", + "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", + "management.tracing.baggage.correlation.fields=country-code,bp"); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java index cf8d945709..baecb0bb3b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import brave.Tracer; import brave.Tracing; +import brave.baggage.BaggagePropagation; import brave.http.HttpClientHandler; import brave.http.HttpClientRequest; import brave.http.HttpClientResponse; @@ -26,6 +27,7 @@ import brave.http.HttpServerRequest; import brave.http.HttpServerResponse; import brave.http.HttpTracing; import brave.propagation.CurrentTraceContext; +import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; import brave.sampler.Sampler; import io.micrometer.tracing.brave.bridge.BraveBaggageManager; @@ -66,9 +68,10 @@ class BraveAutoConfigurationTests { assertThat(context).hasSingleBean(HttpServerHandler.class); assertThat(context).hasSingleBean(HttpClientHandler.class); assertThat(context).hasSingleBean(BraveTracer.class); - assertThat(context).hasSingleBean(BraveBaggageManager.class); assertThat(context).hasSingleBean(BraveHttpServerHandler.class); assertThat(context).hasSingleBean(BraveHttpClientHandler.class); + assertThat(context).hasSingleBean(Propagation.Factory.class); + assertThat(context).hasSingleBean(BaggagePropagation.FactoryBuilder.class); }); } @@ -99,10 +102,22 @@ class BraveAutoConfigurationTests { assertThat(context).hasSingleBean(BraveHttpServerHandler.class); assertThat(context).hasBean("customBraveHttpClientHandler"); assertThat(context).hasSingleBean(BraveHttpClientHandler.class); + assertThat(context).hasBean("customHttpServerHandler"); + assertThat(context).hasSingleBean(HttpServerHandler.class); + assertThat(context).hasBean("customHttpClientHandler"); + assertThat(context).hasSingleBean(HttpClientHandler.class); }); } @Test + void shouldSupplyMicrometerBeans() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(BraveTracer.class); + assertThat(context).hasSingleBean(BraveHttpServerHandler.class); + assertThat(context).hasSingleBean(BraveHttpClientHandler.class); + }); + } + void shouldNotSupplyBeansIfBraveIsMissing() { this.contextRunner.withClassLoader(new FilteredClassLoader("brave")) .run((context) -> assertThat(context).doesNotHaveBean(BraveAutoConfiguration.class)); @@ -114,12 +129,68 @@ class BraveAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(BraveAutoConfiguration.class)); } + @Test + void shouldSupplyW3CPropagationFactoryByDefault() { + this.contextRunner.run((context) -> { + assertThat(context).hasBean("w3cPropagationFactory"); + assertThat(context).hasSingleBean(BaggagePropagation.FactoryBuilder.class); + }); + } + + @Test + void shouldSupplyB3PropagationFactoryViaProperty() { + this.contextRunner.withPropertyValues("management.tracing.propagation.type=B3").run((context) -> { + assertThat(context).hasBean("b3PropagationFactory"); + assertThat(context).hasSingleBean(BaggagePropagation.FactoryBuilder.class); + }); + } + @Test void shouldNotSupplyBeansIfTracingIsDisabled() { this.contextRunner.withPropertyValues("management.tracing.enabled=false") .run((context) -> assertThat(context).doesNotHaveBean(BraveAutoConfiguration.class)); } + @Test + void shouldNotSupplyMdcCorrelationScopeWhenMdcNotOnClasspath() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.slf4j")).run((context) -> { + assertThat(context).doesNotHaveBean("mdcCorrelationScopeDecoratorBuilder"); + assertThat(context).doesNotHaveBean("correlationScopeDecorator"); + }); + } + + @Test + void shouldNotSupplyCorrelationScopeDecoratorIfBaggageDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean("correlationScopeDecorator")); + } + + @Test + void shouldSupplyW3CWithoutBaggageByDefaultIfBaggageDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false") + .run((context) -> assertThat(context).hasBean("w3cPropagationNoBaggageFactory")); + } + + @Test + void shouldSupplyB3WithoutBaggageIfBaggageDisabledAndB3Picked() { + this.contextRunner + .withPropertyValues("management.tracing.baggage.enabled=false", + "management.tracing.propagation.type=B3") + .run((context) -> assertThat(context).hasBean("b3PropagationNoBaggageFactory")); + } + + @Test + void shouldNotSupplyCorrelationScopeDecoratorIfBaggageCorrelationDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean("correlationFieldsCorrelationScopeDecorator")); + } + + @Test + void shouldSupplyMdcCorrelationScopeDecoratorIfBaggageCorrelationDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=false") + .run((context) -> assertThat(context).hasBean("mdcCorrelationScopeDecoratorBuilder")); + } + @Configuration(proxyBeanMethods = false) private static class CustomConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 23440f28b1..1c50fb33ca 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -21,15 +21,21 @@ import io.micrometer.tracing.otel.bridge.OtelHttpClientHandler; import io.micrometer.tracing.otel.bridge.OtelHttpServerHandler; import io.micrometer.tracing.otel.bridge.OtelTracer; import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher; +import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener; +import io.micrometer.tracing.otel.bridge.Slf4JEventListener; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.samplers.Sampler; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Answers; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -63,7 +69,6 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).hasSingleBean(SdkTracerProvider.class); assertThat(context).hasSingleBean(ContextPropagators.class); assertThat(context).hasSingleBean(Sampler.class); - assertThat(context).hasSingleBean(SpanProcessor.class); assertThat(context).hasSingleBean(Tracer.class); }); } @@ -108,12 +113,73 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).hasBean("customSampler"); assertThat(context).hasSingleBean(Sampler.class); assertThat(context).hasBean("customSpanProcessor"); - assertThat(context).hasSingleBean(SpanProcessor.class); assertThat(context).hasBean("customTracer"); assertThat(context).hasSingleBean(Tracer.class); }); } + @Test + void shouldSupplyBaggageAndSlf4jEventListenersWhenMdcOnClasspath() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(Slf4JEventListener.class); + assertThat(context).hasSingleBean(Slf4JBaggageEventListener.class); + }); + } + + @Test + void shouldSupplySlf4jEventListenersWhenMdcOnClasspathAndBaggageCorrelationDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=false").run((context) -> { + assertThat(context).hasSingleBean(Slf4JEventListener.class); + assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class); + }); + } + + @Test + void shouldSupplySlf4jEventListenersWhenMdcOnClasspathAndBaggageDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false").run((context) -> { + assertThat(context).hasSingleBean(Slf4JEventListener.class); + assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class); + }); + } + + @Test + void shouldNotSupplySlf4jEventListenersWhenMdcNotOnClasspath() { + this.contextRunner.withClassLoader(new FilteredClassLoader("org.slf4j")).run((context) -> { + assertThat(context).doesNotHaveBean(Slf4JEventListener.class); + assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class); + }); + } + + @Test + void shouldSupplyB3PropagationIfPropagationPropertySet() { + this.contextRunner.withPropertyValues("management.tracing.propagation.type=B3").run((context) -> { + assertThat(context).hasSingleBean(B3Propagator.class); + assertThat(context).hasBean("b3TextMapPropagator"); + assertThat(context).doesNotHaveBean(W3CTraceContextPropagator.class); + }); + } + + @Test + void shouldSupplyW3CPropagationWithBaggageByDefault() { + this.contextRunner.run((context) -> assertThat(context).hasBean("w3cTextMapPropagatorWithBaggage")); + } + + @Test + void shouldSupplyW3CPropagationWithoutBaggageWhenDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false") + .run((context) -> assertThat(context).hasBean("w3cTextMapPropagatorWithoutBaggage")); + } + + @Test + void shouldSupplyB3PropagationWithoutBaggageWhenBaggageDisabledAndB3PropagationEnabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false", + "management.tracing.propagation.type=B3").run((context) -> { + assertThat(context).hasBean("b3TextMapPropagator"); + assertThat(context).hasSingleBean(B3Propagator.class); + assertThat(context).doesNotHaveBean("w3cTextMapPropagatorWithoutBaggage"); + }); + } + @Configuration(proxyBeanMethods = false) private static class CustomConfiguration { @@ -174,4 +240,34 @@ class OpenTelemetryAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + private static class OpenTelemetryConfiguration { + + @Bean + OpenTelemetry openTelemetry() { + return mock(OpenTelemetry.class, Answers.RETURNS_MOCKS); + } + + } + + @Configuration(proxyBeanMethods = false) + private static class ContextPropagatorsConfiguration { + + @Bean + ContextPropagators contextPropagators() { + return mock(ContextPropagators.class, Answers.RETURNS_MOCKS); + } + + } + + @Configuration(proxyBeanMethods = false) + private static class CustomFactoryConfiguration { + + @Bean + TextMapPropagator customPropagationFactory() { + return mock(TextMapPropagator.class); + } + + } + }