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..670f7caf87 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,15 @@ import brave.Tracer; import brave.Tracing; import brave.Tracing.Builder; import brave.TracingCustomizer; +import brave.baggage.BaggageField; +import brave.baggage.BaggagePropagation; +import brave.baggage.BaggagePropagation.FactoryBuilder; +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; @@ -41,20 +50,27 @@ 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.springframework.beans.factory.ObjectProvider; 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.annotation.Order; 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 +79,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 +123,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 +149,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 +173,92 @@ public class BraveAutoConfiguration { return new BraveHttpClientHandler(httpClientHandler); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.tracing.baggage.enabled", havingValue = "false") + static class BraveNoBaggageConfiguration { + + @Bean + @ConditionalOnMissingBean + Factory propagationFactory(TracingProperties tracing) { + return switch (tracing.getPropagation().getType()) { + case B3 -> + B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE_NO_PARENT).build(); + case W3C -> new W3CPropagation(BRAVE_BAGGAGE_MANAGER, List.of()); + }; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(value = "management.tracing.baggage.enabled", matchIfMissing = true) + static class BraveBaggageConfiguration { + + private final TracingProperties tracingProperties; + + BraveBaggageConfiguration(TracingProperties tracingProperties) { + this.tracingProperties = tracingProperties; + } + + @Bean + @ConditionalOnMissingBean + BaggagePropagation.FactoryBuilder propagationFactoryBuilder( + ObjectProvider baggagePropagationCustomizers) { + Factory delegate = switch (this.tracingProperties.getPropagation().getType()) { + case B3 -> + B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE_NO_PARENT).build(); + case W3C -> new W3CPropagation(BRAVE_BAGGAGE_MANAGER, List.of()); + }; + FactoryBuilder builder = BaggagePropagation.newFactoryBuilder(delegate); + baggagePropagationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + + @Bean + @Order(0) + BaggagePropagationCustomizer remoteFieldsBaggagePropagationCustomizer() { + return (builder) -> { + List remoteFields = this.tracingProperties.getBaggage().getRemoteFields(); + for (String fieldName : remoteFields) { + builder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create(fieldName))); + } + }; + } + + @Bean + @ConditionalOnMissingBean + Factory propagationFactory(BaggagePropagation.FactoryBuilder factoryBuilder) { + return factoryBuilder.build(); + } + + @Bean + @ConditionalOnMissingBean + CorrelationScopeDecorator.Builder mdcCorrelationScopeDecoratorBuilder( + ObjectProvider correlationScopeCustomizers) { + CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder(); + correlationScopeCustomizers.forEach((customizer) -> customizer.customize(builder)); + return builder; + } + + @Bean + @Order(0) + @ConditionalOnProperty(prefix = "management.tracing.baggage.correlation", name = "enabled", + matchIfMissing = true) + CorrelationScopeCustomizer correlationFieldsCorrelationScopeCustomizer() { + return (builder) -> { + List correlationFields = this.tracingProperties.getBaggage().getCorrelation().getFields(); + for (String field : correlationFields) { + builder.add(CorrelationScopeConfig.SingleCorrelationField.newBuilder(BaggageField.create(field)) + .flushOnUpdate().build()); + } + }; + } + + @Bean + @ConditionalOnMissingBean(CorrelationScopeDecorator.class) + ScopeDecorator correlationScopeDecorator(CorrelationScopeDecorator.Builder builder) { + return builder.build(); + } + + } + } 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..c5efef0e6c 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; @@ -44,13 +54,16 @@ import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.springframework.beans.factory.ObjectProvider; 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; /** @@ -70,6 +83,12 @@ public class OpenTelemetryAutoConfiguration { */ private static final String DEFAULT_APPLICATION_NAME = "application"; + private final TracingProperties tracingProperties; + + OpenTelemetryAutoConfiguration(TracingProperties tracingProperties) { + this.tracingProperties = tracingProperties; + } + @Bean @ConditionalOnMissingBean OpenTelemetry openTelemetry(SdkTracerProvider sdkTracerProvider, ContextPropagators contextPropagators) { @@ -79,33 +98,31 @@ public class OpenTelemetryAutoConfiguration { @Bean @ConditionalOnMissingBean - SdkTracerProvider otelSdkTracerProvider(Environment environment, List spanProcessors, + SdkTracerProvider otelSdkTracerProvider(Environment environment, ObjectProvider spanProcessors, Sampler sampler) { String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); SdkTracerProviderBuilder builder = SdkTracerProvider.builder().setSampler(sampler) .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))); - for (SpanProcessor spanProcessor : spanProcessors) { - builder.addSpanProcessor(spanProcessor); - } + spanProcessors.orderedStream().forEach(builder::addSpanProcessor); return builder.build(); } @Bean @ConditionalOnMissingBean - ContextPropagators otelContextPropagators(List textMapPropagators) { - return ContextPropagators.create(TextMapPropagator.composite(textMapPropagators)); + ContextPropagators otelContextPropagators(ObjectProvider textMapPropagators) { + return ContextPropagators + .create(TextMapPropagator.composite(textMapPropagators.orderedStream().collect(Collectors.toList()))); } @Bean @ConditionalOnMissingBean - Sampler otelSampler(TracingProperties properties) { - return Sampler.traceIdRatioBased(properties.getSampling().getProbability()); + Sampler otelSampler() { + return Sampler.traceIdRatioBased(this.tracingProperties.getSampling().getProbability()); } @Bean - @ConditionalOnMissingBean - SpanProcessor otelSpanProcessor(List spanExporter) { - return SpanProcessor.composite(spanExporter.stream() + SpanProcessor otelSpanProcessor(ObjectProvider spanExporters) { + return SpanProcessor.composite(spanExporters.orderedStream() .map((exporter) -> BatchSpanProcessor.builder(exporter).build()).collect(Collectors.toList())); } @@ -119,20 +136,26 @@ public class OpenTelemetryAutoConfiguration { @ConditionalOnMissingBean OtelTracer micrometerOtelTracer(Tracer tracer, EventPublisher eventPublisher, OtelCurrentTraceContext otelCurrentTraceContext) { - return new OtelTracer(tracer, otelCurrentTraceContext, eventPublisher, - new OtelBaggageManager(otelCurrentTraceContext, List.of(), List.of())); + return new OtelTracer(tracer, otelCurrentTraceContext, eventPublisher, new OtelBaggageManager( + otelCurrentTraceContext, this.tracingProperties.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 +173,84 @@ public class OpenTelemetryAutoConfiguration { new DefaultHttpServerAttributesExtractor()); } + @Bean + @ConditionalOnMissingBean + Slf4JEventListener otelSlf4JEventListener() { + return new Slf4JEventListener(); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "management.tracing.baggage", name = "enabled", matchIfMissing = true) + static class BaggageConfiguration { + + private final TracingProperties tracingProperties; + + BaggageConfiguration(TracingProperties tracingProperties) { + this.tracingProperties = tracingProperties; + } + + @Bean + @ConditionalOnProperty(prefix = "management.tracing.propagation", name = "type", havingValue = "W3C", + matchIfMissing = true) + TextMapPropagator w3cTextMapPropagatorWithBaggage() { + return TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), + W3CBaggagePropagator.getInstance()); + } + + @Bean + @ConditionalOnProperty(prefix = "management.tracing.propagation", name = "type", havingValue = "B3") + TextMapPropagator b3BaggageTextMapPropagator(OtelCurrentTraceContext otelCurrentTraceContext) { + List remoteFields = this.tracingProperties.getBaggage().getRemoteFields(); + return TextMapPropagator.composite(B3Propagator.injectingSingleHeader(), new BaggageTextMapPropagator( + remoteFields, new OtelBaggageManager(otelCurrentTraceContext, remoteFields, List.of()))); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "management.tracing.baggage.correlation", name = "enabled", + matchIfMissing = true) + Slf4JBaggageEventListener otelSlf4JBaggageEventListener() { + return new Slf4JBaggageEventListener(this.tracingProperties.getBaggage().getCorrelation().getFields()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "management.tracing.baggage", name = "enabled", havingValue = "false") + static class NoBaggageConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "management.tracing.propagation", name = "type", havingValue = "B3") + B3Propagator b3TextMapPropagator() { + return B3Propagator.injectingSingleHeader(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "management.tracing.propagation", name = "type", havingValue = "W3C", + matchIfMissing = true) + W3CTraceContextPropagator w3cTextMapPropagatorWithoutBaggage() { + return W3CTraceContextPropagator.getInstance(); + } + + } + + 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..1aca4a5339 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 = true; + + /** + * 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/BaggagePropagationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java new file mode 100644 index 0000000000..2d4fb67902 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java @@ -0,0 +1,183 @@ +/* + * 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 propagation with Brave and OpenTelemetry using W3C and B3 propagation + * formats. + * + * @author Marcin Grzejszczak + */ +class BaggagePropagationIntegrationTests { + + 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..1bac894b1b 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,8 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import brave.Tracer; import brave.Tracing; +import brave.baggage.BaggagePropagation; +import brave.baggage.CorrelationScopeConfig.SingleCorrelationField; import brave.http.HttpClientHandler; import brave.http.HttpClientRequest; import brave.http.HttpClientResponse; @@ -26,12 +28,16 @@ import brave.http.HttpServerRequest; import brave.http.HttpServerResponse; import brave.http.HttpTracing; import brave.propagation.CurrentTraceContext; +import brave.propagation.CurrentTraceContext.ScopeDecorator; +import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; import brave.sampler.Sampler; import io.micrometer.tracing.brave.bridge.BraveBaggageManager; import io.micrometer.tracing.brave.bridge.BraveHttpClientHandler; import io.micrometer.tracing.brave.bridge.BraveHttpServerHandler; import io.micrometer.tracing.brave.bridge.BraveTracer; +import io.micrometer.tracing.brave.bridge.W3CPropagation; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.mockito.Answers; @@ -57,6 +63,7 @@ class BraveAutoConfigurationTests { @Test void shouldSupplyDefaultBeans() { this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(BraveAutoConfiguration.class); assertThat(context).hasSingleBean(Tracing.class); assertThat(context).hasSingleBean(Tracer.class); assertThat(context).hasSingleBean(CurrentTraceContext.class); @@ -66,7 +73,11 @@ 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); + assertThat(context).hasSingleBean(BraveTracer.class); assertThat(context).hasSingleBean(BraveHttpServerHandler.class); assertThat(context).hasSingleBean(BraveHttpClientHandler.class); }); @@ -99,6 +110,19 @@ 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); }); } @@ -110,16 +134,83 @@ class BraveAutoConfigurationTests { @Test void shouldNotSupplyBeansIfMicrometerIsMissing() { - this.contextRunner.withClassLoader(new FilteredClassLoader("brave")) + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer")) .run((context) -> assertThat(context).doesNotHaveBean(BraveAutoConfiguration.class)); } + @Test + void shouldSupplyW3CPropagationFactoryByDefault() { + this.contextRunner.run((context) -> { + assertThat(context).hasBean("propagationFactory"); + assertThat(context).hasSingleBean(W3CPropagation.class); + assertThat(context).hasSingleBean(BaggagePropagation.FactoryBuilder.class); + }); + } + + @Test + void shouldSupplyB3PropagationFactoryViaProperty() { + this.contextRunner.withPropertyValues("management.tracing.propagation.type=B3").run((context) -> { + assertThat(context).hasBean("propagationFactory"); + assertThat(context.getBean(Factory.class).toString()).isEqualTo("B3Propagation"); + 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 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).hasSingleBean(W3CPropagation.class)); + } + + @Test + void shouldSupplyB3WithoutBaggageIfBaggageDisabledAndB3Picked() { + this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false", + "management.tracing.propagation.type=B3").run((context) -> { + assertThat(context).hasBean("propagationFactory"); + assertThat(context.getBean(Factory.class).toString()).isEqualTo("B3Propagation"); + }); + } + + @Test + void shouldNotApplyCorrelationFieldsIfBaggageCorrelationDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=false", + "management.tracing.baggage.correlation.fields=alpha,bravo").run((context) -> { + ScopeDecorator scopeDecorator = context.getBean(ScopeDecorator.class); + assertThat(scopeDecorator) + .extracting("fields", InstanceOfAssertFactories.array(SingleCorrelationField[].class)) + .hasSize(2); + }); + } + + @Test + void shouldNotApplyCorrelationFieldsIfBaggageCorrelationEnabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=true", + "management.tracing.baggage.correlation.fields=alpha,bravo").run((context) -> { + ScopeDecorator scopeDecorator = context.getBean(ScopeDecorator.class); + assertThat(scopeDecorator) + .extracting("fields", InstanceOfAssertFactories.array(SingleCorrelationField[].class)) + .hasSize(4); + }); + } + + @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..21f3ed7e5c 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 @@ -16,20 +16,29 @@ package org.springframework.boot.actuate.autoconfigure.tracing; +import java.util.List; + 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.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,8 +72,12 @@ 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); + assertThat(context).hasSingleBean(Slf4JEventListener.class); + assertThat(context).hasSingleBean(Slf4JBaggageEventListener.class); + assertThat(context).hasSingleBean(SpanProcessor.class); + assertThat(context).hasSingleBean(OtelPropagator.class); + assertThat(context).hasSingleBean(TextMapPropagator.class); }); } @@ -81,8 +94,12 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).doesNotHaveBean(SdkTracerProvider.class); assertThat(context).doesNotHaveBean(ContextPropagators.class); assertThat(context).doesNotHaveBean(Sampler.class); - assertThat(context).doesNotHaveBean(SpanProcessor.class); assertThat(context).doesNotHaveBean(Tracer.class); + assertThat(context).doesNotHaveBean(Slf4JEventListener.class); + assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class); + assertThat(context).doesNotHaveBean(SpanProcessor.class); + assertThat(context).doesNotHaveBean(OtelPropagator.class); + assertThat(context).doesNotHaveBean(TextMapPropagator.class); }); } @@ -107,13 +124,84 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).hasSingleBean(ContextPropagators.class); 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); + assertThat(context).hasBean("customSlf4jEventListener"); + assertThat(context).hasSingleBean(Slf4JEventListener.class); + assertThat(context).hasBean("customSlf4jBaggageEventListener"); + assertThat(context).hasSingleBean(Slf4JBaggageEventListener.class); + assertThat(context).hasBean("customOtelPropagator"); + assertThat(context).hasSingleBean(OtelPropagator.class); }); } + @Test + void shouldAllowMultipleSpanProcessors() { + this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { + assertThat(context.getBeansOfType(SpanProcessor.class)).hasSize(2); + assertThat(context).hasBean("customSpanProcessor"); + }); + } + + @Test + void shouldAllowMultipleTextMapPropagators() { + this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { + assertThat(context.getBeansOfType(TextMapPropagator.class)).hasSize(2); + assertThat(context).hasBean("customTextMapPropagator"); + }); + } + + @Test + void shouldNotSupplySlf4jBaggageEventListenerBaggageCorrelationDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class)); + } + + @Test + void shouldNotSupplySlf4JBaggageEventListenerWhenBaggageDisabled() { + this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class)); + } + + @Test + void shouldSupplyB3PropagationIfPropagationPropertySet() { + this.contextRunner.withPropertyValues("management.tracing.propagation.type=B3").run((context) -> { + assertThat(context).hasBean("b3BaggageTextMapPropagator"); + assertThat(context).doesNotHaveBean(W3CTraceContextPropagator.class); + }); + } + + @Test + void shouldSupplyB3PropagationIfPropagationPropertySetAndBaggageDisabled() { + this.contextRunner.withPropertyValues("management.tracing.propagation.type=B3", + "management.tracing.baggage.enabled=false").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 { @@ -172,6 +260,56 @@ class OpenTelemetryAutoConfigurationTests { return mock(Tracer.class); } + @Bean + Slf4JEventListener customSlf4jEventListener() { + return new Slf4JEventListener(); + } + + @Bean + Slf4JBaggageEventListener customSlf4jBaggageEventListener() { + return new Slf4JBaggageEventListener(List.of("alpha")); + } + + @Bean + OtelPropagator customOtelPropagator(ContextPropagators propagators, Tracer tracer) { + return new OtelPropagator(propagators, tracer); + } + + @Bean + TextMapPropagator customTextMapPropagator() { + return mock(TextMapPropagator.class); + } + + } + + @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); + } + } }