diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/ExemplarsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/ExemplarsAutoConfiguration.java new file mode 100644 index 00000000000..8eb0940f669 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/ExemplarsAutoConfiguration.java @@ -0,0 +1,93 @@ +/* + * 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.exemplars; + +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; +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.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Prometheus Exemplars with + * Micrometer Tracing. + * + * @author Jonatan Ivanov + * @since 3.0.0 + */ +@AutoConfiguration(before = PrometheusMetricsExportAutoConfiguration.class) +@ConditionalOnClass({ Tracer.class, SpanContextSupplier.class }) +@ConditionalOnEnabledTracing +public class ExemplarsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + SpanContextSupplier spanContextSupplier(ObjectProvider tracerProvider) { + return new LazyTracingSpanContextSupplier(tracerProvider); + } + + /** + * Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the + * {@link Tracer} can depend on the MeterRegistry (recording metrics), this + * {@link SpanContextSupplier} breaks the circle by lazily loading the {@link Tracer}. + */ + static class LazyTracingSpanContextSupplier implements SpanContextSupplier, SmartInitializingSingleton { + + private final ObjectProvider tracerProvider; + + private Tracer tracer; + + LazyTracingSpanContextSupplier(ObjectProvider tracerProvider) { + this.tracerProvider = tracerProvider; + } + + @Override + public String getTraceId() { + return this.tracer.currentSpan().context().traceId(); + } + + @Override + public String getSpanId() { + return this.tracer.currentSpan().context().spanId(); + } + + @Override + public boolean isSampled() { + return this.tracer != null && isSampled(this.tracer); + } + + private boolean isSampled(Tracer tracer) { + Span currentSpan = tracer.currentSpan(); + return currentSpan != null && currentSpan.context().sampled(); + } + + @Override + public void afterSingletonsInstantiated() { + this.tracer = this.tracerProvider.getIfAvailable(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/package-info.java new file mode 100644 index 00000000000..4fde3ff674a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Auto-configuration for Prometheus Exemplars with Micrometer Tracing. + */ +package org.springframework.boot.actuate.autoconfigure.tracing.exemplars; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1ba70ac93f7..2bdd69ff79b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -101,6 +101,7 @@ org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoC org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration +org.springframework.boot.actuate.autoconfigure.tracing.exemplars.ExemplarsAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/ExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/ExemplarsAutoConfigurationTests.java new file mode 100644 index 00000000000..5eb67e3f6b7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/exemplars/ExemplarsAutoConfigurationTests.java @@ -0,0 +1,105 @@ +/* + * 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.exemplars; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import io.prometheus.client.exporter.common.TextFormat; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ExemplarsAutoConfiguration}. + * + * * @author Jonatan Ivanov + */ +class ExemplarsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.tracing.sampling.probability=1.0", + "management.metrics.distribution.percentiles-histogram.all=true") + .with(MetricsRun.limitedTo(PrometheusMetricsExportAutoConfiguration.class)).withConfiguration( + AutoConfigurations.of(ExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, + BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); + + @Test + void shouldNotSupplyBeansIfTracingIsDisabled() { + this.contextRunner.withPropertyValues("management.tracing.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void shouldNotSupplyBeansIfPrometheusSupportIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.client.exemplars")) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void shouldNotSupplyBeansIfMicrometerTracingIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing")) + .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void shouldSupplyCustomBeans() { + this.contextRunner.withUserConfiguration(CustomConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(SpanContextSupplier.class) + .getBean(SpanContextSupplier.class).isSameAs(CustomConfiguration.SUPPLIER)); + } + + @Test + void prometheusOpenMetricsOutputShouldContainExemplars() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(SpanContextSupplier.class); + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test.observation", observationRegistry).stop(); + + PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); + String openMetricsOutput = prometheusMeterRegistry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100); + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket").containsOnlyOnce("trace_id=") + .containsOnlyOnce("span_id="); + }); + } + + @Configuration(proxyBeanMethods = false) + private static class CustomConfiguration { + + static final SpanContextSupplier SUPPLIER = mock(SpanContextSupplier.class); + + @Bean + SpanContextSupplier customSpanContextSupplier() { + return SUPPLIER; + } + + } + +}