diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index a01ccc32b..3cc5a4d1f 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -801,3 +801,11 @@ In order to disable this instrumentation set `spring.sleuth.session.enabled` to This feature is available for all tracer implementations. We're adding Kotlin Coroutines that allow you to retrieve the current span via the `Tracer` bean. You can either pass the bean to the Kotlin Coroutine context via `Tracer.asContextElement()` method execution or if you have Reactor Kotlin Coroutine integration on the classpath, we will retrieve it from Reactor's context. To retrieve the current span you can call the `currentSpan()` method within the Kotlin Coroutine. + +[[sleuth-prometheus-exemplars-integration]] +== Prometheus Exemplars + +This feature is available for all tracer implementations. + +https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Exemplars] are supported through `SpanContextSupplier`. If you use https://micrometer.io[Micrometer], this will be auto-configured for you, but you can register `SpanContextSupplier` directly to Prometheus if you want. + +Please check the https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage[Prometheus Docs], since this feature needs to be explicitly enabled on Prometheus' side, and it is only supported using the https://github.com/OpenObservability/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#exemplars[OpenMetrics] format. diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 028d8bb18..d08c895c4 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -222,6 +222,11 @@ HikariCP true + + io.prometheus + simpleclient_common + true + org.springframework.session spring-session-data-redis diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/prometheus/PrometheusExemplarsAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/prometheus/PrometheusExemplarsAutoConfiguration.java new file mode 100644 index 000000000..9fb5ba61e --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022-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.cloud.sleuth.autoconfig.instrument.prometheus; + +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.prometheus.prometheus.LazySleuthSpanContextSupplier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} enables Prometheus Exemplars support. + * + * @author Jonatan Ivanov + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(SpanContextSupplier.class) +@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) +@AutoConfigureBefore( + name = "org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration") +public class PrometheusExemplarsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "spring.sleuth.prometheus.exemplars.enabled", matchIfMissing = true) + SpanContextSupplier sleuthSpanContextSupplier(ObjectProvider tracerProvider) { + return new LazySleuthSpanContextSupplier(tracerProvider); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 7f3273cbf..040841570 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -15,6 +15,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircu org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.mongodb.TraceMongoDbAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.prometheus.PrometheusExemplarsAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.task.TraceTaskAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/prometheus/PrometheusExemplarsAutoConfigurationTests.java new file mode 100644 index 000000000..64e852483 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2022-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.cloud.sleuth.autoconfig.instrument.prometheus; + +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.prometheus.prometheus.SleuthSpanContextSupplier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PrometheusExemplarsAutoConfiguration}. + * + * @author Jonatan Ivanov + */ +class PrometheusExemplarsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration(AutoConfigurations + .of(TraceNoOpAutoConfiguration.class, PrometheusExemplarsAutoConfiguration.class)); + + @Test + void should_register_span_context_supplier() { + contextRunner.run(context -> assertThat(context).hasSingleBean(SpanContextSupplier.class)); + } + + @Test + void should_not_register_span_context_supplier_if_sleuth_disabled() { + contextRunner.withPropertyValues("spring.sleuth.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void should_not_register_span_context_supplier_if_exemplars_disabled() { + contextRunner.withPropertyValues("spring.sleuth.prometheus.exemplars.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void should_not_register_span_context_supplier_if_class_is_missing() { + contextRunner.withClassLoader(new FilteredClassLoader(SpanContextSupplier.class)) + .run(context -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); + } + + @Test + void should_not_register_span_context_supplier_if_bean_exists() { + contextRunner.withUserConfiguration(TestPrometheusExemplarsAutoConfiguration.class).run(context -> { + assertThat(context).hasSingleBean(SpanContextSupplier.class); + assertThat(context.getBean(SpanContextSupplier.class)) + .isSameAs(TestPrometheusExemplarsAutoConfiguration.SUPPLIER); + }); + } + + @Configuration(proxyBeanMethods = false) + static class TestPrometheusExemplarsAutoConfiguration { + + static final SpanContextSupplier SUPPLIER = new SleuthSpanContextSupplier(null); + + @Bean + SpanContextSupplier testSpanContextSupplier() { + return SUPPLIER; + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/prometheus/prometheus/LazySleuthSpanContextSupplier.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/prometheus/prometheus/LazySleuthSpanContextSupplier.java new file mode 100644 index 000000000..7b8d380e9 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/prometheus/prometheus/LazySleuthSpanContextSupplier.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021-2021 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.cloud.sleuth.instrument.prometheus.prometheus; + +import io.prometheus.client.exemplars.tracer.common.SpanContextSupplier; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.cloud.sleuth.Tracer; + +/** + * A {@link SpanContextSupplier} that fetches the {@link Tracer} lazily after singletons + * are instantiated and uses it onwards. + * + * @author Jonatan Ivanov + */ +public class LazySleuthSpanContextSupplier implements SpanContextSupplier, SmartInitializingSingleton { + + private final ObjectProvider tracerProvider; + + private SpanContextSupplier delegate; + + public LazySleuthSpanContextSupplier(ObjectProvider tracerProvider) { + this.tracerProvider = tracerProvider; + } + + @Override + public String getTraceId() { + return (this.delegate != null) ? this.delegate.getTraceId() : null; + } + + @Override + public String getSpanId() { + return (this.delegate != null) ? this.delegate.getSpanId() : null; + } + + @Override + public void afterSingletonsInstantiated() { + Tracer tracer = tracerProvider.getIfAvailable(); + if (tracer != null) { + this.delegate = new SleuthSpanContextSupplier(tracer); + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/prometheus/LazySleuthSpanContextSupplierTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/prometheus/LazySleuthSpanContextSupplierTests.java new file mode 100644 index 000000000..1fe70d0a8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/prometheus/LazySleuthSpanContextSupplierTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022-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.cloud.sleuth.instrument.prometheus; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.prometheus.prometheus.LazySleuthSpanContextSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link LazySleuthSpanContextSupplier}. + * + * @author Jonatan Ivanov + */ +class LazySleuthSpanContextSupplierTests { + + @Test + void should_provide_null_values_if_not_initialized() { + LazySleuthSpanContextSupplier spanContextSupplier = new LazySleuthSpanContextSupplier(null); + assertThat(spanContextSupplier.getTraceId()).isNull(); + assertThat(spanContextSupplier.getSpanId()).isNull(); + } + + @Test + void should_provide_null_values_if_initialized_without_delegate() { + ObjectProvider tracerProvider = mock(ObjectProvider.class); + when(tracerProvider.getIfAvailable()).thenReturn(null); + + LazySleuthSpanContextSupplier spanContextSupplier = new LazySleuthSpanContextSupplier(tracerProvider); + spanContextSupplier.afterSingletonsInstantiated(); + assertThat(spanContextSupplier.getTraceId()).isNull(); + assertThat(spanContextSupplier.getSpanId()).isNull(); + } + + @Test + void should_provide_values_from_delegate_if_initialized() { + ObjectProvider tracerProvider = mock(ObjectProvider.class); + Tracer tracer = mock(Tracer.class); + Span span = mock(Span.class); + TraceContext context = mock(TraceContext.class); + + when(tracerProvider.getIfAvailable()).thenReturn(tracer); + when(tracer.currentSpan()).thenReturn(span); + when(span.context()).thenReturn(context); + when(context.sampled()).thenReturn(true); + when(context.traceId()).thenReturn("42"); + when(context.spanId()).thenReturn("24"); + + LazySleuthSpanContextSupplier spanContextSupplier = new LazySleuthSpanContextSupplier(tracerProvider); + spanContextSupplier.afterSingletonsInstantiated(); + assertThat(spanContextSupplier.getTraceId()).isEqualTo("42"); + assertThat(spanContextSupplier.getSpanId()).isEqualTo("24"); + } + +}