diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java index 29e89c29e5..5ee64e539e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -24,6 +24,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegi import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -36,6 +37,7 @@ import org.springframework.context.annotation.Bean; * {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to OTLP. * * @author Eddú Meléndez + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration( @@ -44,19 +46,23 @@ import org.springframework.context.annotation.Bean; @ConditionalOnBean(Clock.class) @ConditionalOnClass(OtlpMeterRegistry.class) @ConditionalOnEnabledMetricsExport("otlp") -@EnableConfigurationProperties(OtlpProperties.class) +@EnableConfigurationProperties({ OtlpProperties.class, OpenTelemetryProperties.class }) public class OtlpMetricsExportAutoConfiguration { private final OtlpProperties properties; - public OtlpMetricsExportAutoConfiguration(OtlpProperties properties) { + private final OpenTelemetryProperties openTelemetryProperties; + + public OtlpMetricsExportAutoConfiguration(OtlpProperties properties, + OpenTelemetryProperties openTelemetryProperties) { this.properties = properties; + this.openTelemetryProperties = openTelemetryProperties; } @Bean @ConditionalOnMissingBean public OtlpConfig otlpConfig() { - return new OtlpPropertiesConfigAdapter(this.properties); + return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java index b39faa5577..ba3cc1145b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java @@ -23,6 +23,7 @@ import io.micrometer.registry.otlp.AggregationTemporality; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * {@link ConfigurationProperties @ConfigurationProperties} for configuring OTLP metrics @@ -77,6 +78,7 @@ public class OtlpProperties extends StepRegistryProperties { this.aggregationTemporality = aggregationTemporality; } + @DeprecatedConfigurationProperty(replacement = "management.opentelemetry.resource-attributes") public Map getResourceAttributes() { return this.resourceAttributes; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index e21455e80f..2d225499cb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -23,17 +23,23 @@ import io.micrometer.registry.otlp.AggregationTemporality; import io.micrometer.registry.otlp.OtlpConfig; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; +import org.springframework.util.CollectionUtils; /** * Adapter to convert {@link OtlpProperties} to an {@link OtlpConfig}. * * @author Eddú Meléndez * @author Jonatan Ivanov + * @author Moritz Halbritter */ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter implements OtlpConfig { - OtlpPropertiesConfigAdapter(OtlpProperties properties) { + private final OpenTelemetryProperties openTelemetryProperties; + + OtlpPropertiesConfigAdapter(OtlpProperties properties, OpenTelemetryProperties openTelemetryProperties) { super(properties); + this.openTelemetryProperties = openTelemetryProperties; } @Override @@ -53,6 +59,9 @@ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter resourceAttributes() { + if (!CollectionUtils.isEmpty(this.openTelemetryProperties.getResourceAttributes())) { + return this.openTelemetryProperties.getResourceAttributes(); + } return get(OtlpProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java new file mode 100644 index 0000000000..92bb18e374 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.opentelemetry; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; + +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.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@AutoConfiguration +@ConditionalOnClass(OpenTelemetrySdk.class) +@EnableConfigurationProperties(OpenTelemetryProperties.class) +public class OpenTelemetryAutoConfiguration { + + /** + * Default value for application name if {@code spring.application.name} is not set. + */ + private static final String DEFAULT_APPLICATION_NAME = "application"; + + @Bean + @ConditionalOnMissingBean(OpenTelemetry.class) + OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, + ObjectProvider propagators, ObjectProvider loggerProvider, + ObjectProvider meterProvider) { + OpenTelemetrySdkBuilder builder = OpenTelemetrySdk.builder(); + tracerProvider.ifAvailable(builder::setTracerProvider); + propagators.ifAvailable(builder::setPropagators); + loggerProvider.ifAvailable(builder::setLoggerProvider); + meterProvider.ifAvailable(builder::setMeterProvider); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { + String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); + return Resource.getDefault() + .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))) + .merge(properties.toResource()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java new file mode 100644 index 0000000000..4b2b68da0c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.opentelemetry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for OpenTelemetry. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@ConfigurationProperties(prefix = "management.opentelemetry") +public class OpenTelemetryProperties { + + /** + * Resource attributes. + */ + private Map resourceAttributes = new HashMap<>(); + + public Map getResourceAttributes() { + return this.resourceAttributes; + } + + public void setResourceAttributes(Map resourceAttributes) { + this.resourceAttributes = resourceAttributes; + } + + Resource toResource() { + ResourceBuilder builder = Resource.builder(); + for (Entry entry : this.resourceAttributes.entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/package-info.java new file mode 100644 index 0000000000..c1aab18823 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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 OpenTelemetry. + */ +package org.springframework.boot.actuate.autoconfigure.opentelemetry; 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 adb79ab09b..92e75afc07 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 @@ -36,13 +36,11 @@ 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.common.Attributes; import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; @@ -51,7 +49,6 @@ import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; 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; @@ -63,26 +60,20 @@ 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 OpenTelemetry. + * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry tracing. * * @author Moritz Halbritter * @author Marcin Grzejszczak * @author Yanming Zhou * @since 3.0.0 */ -@AutoConfiguration(before = MicrometerTracingAutoConfiguration.class) +@AutoConfiguration(value = "openTelemetryTracingAutoConfiguration", before = MicrometerTracingAutoConfiguration.class) @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class }) @EnableConfigurationProperties(TracingProperties.class) public class OpenTelemetryAutoConfiguration { - /** - * Default value for application name if {@code spring.application.name} is not set. - */ - private static final String DEFAULT_APPLICATION_NAME = "application"; - private final TracingProperties tracingProperties; OpenTelemetryAutoConfiguration(TracingProperties tracingProperties) { @@ -91,22 +82,9 @@ public class OpenTelemetryAutoConfiguration { @Bean @ConditionalOnMissingBean - OpenTelemetry openTelemetry(SdkTracerProvider sdkTracerProvider, ContextPropagators contextPropagators) { - return OpenTelemetrySdk.builder() - .setTracerProvider(sdkTracerProvider) - .setPropagators(contextPropagators) - .build(); - } - - @Bean - @ConditionalOnMissingBean - SdkTracerProvider otelSdkTracerProvider(Environment environment, SpanProcessors spanProcessors, Sampler sampler, + SdkTracerProvider otelSdkTracerProvider(Resource resource, SpanProcessors spanProcessors, Sampler sampler, ObjectProvider customizers) { - String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); - Resource springResource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName)); - SdkTracerProviderBuilder builder = SdkTracerProvider.builder() - .setSampler(sampler) - .setResource(Resource.getDefault().merge(springResource)); + SdkTracerProviderBuilder builder = SdkTracerProvider.builder().setSampler(sampler).setResource(resource); spanProcessors.forEach(builder::addSpanProcessor); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); 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 eb46381bf1..79c1000625 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 @@ -88,6 +88,7 @@ org.springframework.boot.actuate.autoconfigure.data.mongo.MongoReactiveHealthCon org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration +org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.r2dbc.R2dbcObservationAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index 87f527dcd1..5d6cbea3ff 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -16,69 +16,93 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; +import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link OtlpPropertiesConfigAdapter}. * * @author Eddú Meléndez + * @author Moritz Halbritter */ class OtlpPropertiesConfigAdapterTests { + private OtlpProperties properties; + + private OpenTelemetryProperties openTelemetryProperties; + + @BeforeEach + void setUp() { + this.properties = new OtlpProperties(); + this.openTelemetryProperties = new OpenTelemetryProperties(); + } + @Test void whenPropertiesUrlIsSetAdapterUrlReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setUrl("http://another-url:4318/v1/metrics"); - assertThat(new OtlpPropertiesConfigAdapter(properties).url()).isEqualTo("http://another-url:4318/v1/metrics"); + this.properties.setUrl("http://another-url:4318/v1/metrics"); + assertThat(createAdapter().url()).isEqualTo("http://another-url:4318/v1/metrics"); } @Test void whenPropertiesAggregationTemporalityIsNotSetAdapterAggregationTemporalityReturnsCumulative() { - OtlpProperties properties = new OtlpProperties(); - assertThat(new OtlpPropertiesConfigAdapter(properties).aggregationTemporality()) - .isSameAs(AggregationTemporality.CUMULATIVE); + assertThat(createAdapter().aggregationTemporality()).isSameAs(AggregationTemporality.CUMULATIVE); } @Test void whenPropertiesAggregationTemporalityIsSetAdapterAggregationTemporalityReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setAggregationTemporality(AggregationTemporality.DELTA); - assertThat(new OtlpPropertiesConfigAdapter(properties).aggregationTemporality()) - .isSameAs(AggregationTemporality.DELTA); + this.properties.setAggregationTemporality(AggregationTemporality.DELTA); + assertThat(createAdapter().aggregationTemporality()).isSameAs(AggregationTemporality.DELTA); } @Test void whenPropertiesResourceAttributesIsSetAdapterResourceAttributesReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setResourceAttributes(Map.of("service.name", "boot-service")); - assertThat(new OtlpPropertiesConfigAdapter(properties).resourceAttributes()).containsEntry("service.name", - "boot-service"); + this.properties.setResourceAttributes(Map.of("service.name", "boot-service")); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "boot-service"); } @Test void whenPropertiesHeadersIsSetAdapterHeadersReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setHeaders(Map.of("header", "value")); - assertThat(new OtlpPropertiesConfigAdapter(properties).headers()).containsEntry("header", "value"); + this.properties.setHeaders(Map.of("header", "value")); + assertThat(createAdapter().headers()).containsEntry("header", "value"); } @Test void whenPropertiesBaseTimeUnitIsNotSetAdapterBaseTimeUnitReturnsMillis() { - OtlpProperties properties = new OtlpProperties(); - assertThat(new OtlpPropertiesConfigAdapter(properties).baseTimeUnit()).isSameAs(TimeUnit.MILLISECONDS); + assertThat(createAdapter().baseTimeUnit()).isSameAs(TimeUnit.MILLISECONDS); } @Test void whenPropertiesBaseTimeUnitIsSetAdapterBaseTimeUnitReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setBaseTimeUnit(TimeUnit.SECONDS); - assertThat(new OtlpPropertiesConfigAdapter(properties).baseTimeUnit()).isSameAs(TimeUnit.SECONDS); + this.properties.setBaseTimeUnit(TimeUnit.SECONDS); + assertThat(createAdapter().baseTimeUnit()).isSameAs(TimeUnit.SECONDS); + } + + @Test + void openTelemetryPropertiesShouldOverrideOtlpPropertiesIfNotEmpty() { + this.properties.setResourceAttributes(Map.of("a", "alpha")); + this.openTelemetryProperties.setResourceAttributes(Map.of("b", "beta")); + assertThat(createAdapter().resourceAttributes()).containsExactly(entry("b", "beta")); + } + + @Test + void openTelemetryPropertiesShouldNotOverrideOtlpPropertiesIfEmpty() { + this.properties.setResourceAttributes(Map.of("a", "alpha")); + this.openTelemetryProperties.setResourceAttributes(Collections.emptyMap()); + assertThat(createAdapter().resourceAttributes()).containsExactly(entry("a", "alpha")); + } + + private OtlpPropertiesConfigAdapter createAdapter() { + return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java new file mode 100644 index 0000000000..e6fbbcf5dd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-2023 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.opentelemetry; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.annotation.ImportCandidates; +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.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OpenTelemetryAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class OpenTelemetryAutoConfigurationTests { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)); + + @Test + void isRegisteredInAutoConfigurationImports() { + assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates()) + .contains(OpenTelemetryAutoConfiguration.class.getName()); + } + + @Test + void shouldProvideBeans() { + this.runner.run((context) -> { + assertThat(context).hasSingleBean(OpenTelemetrySdk.class); + assertThat(context).hasSingleBean(Resource.class); + }); + } + + @Test + void shouldBackOffIfOpenTelemetryIsNotOnClasspath() { + this.runner.withClassLoader(new FilteredClassLoader("io.opentelemetry")).run((context) -> { + assertThat(context).doesNotHaveBean(OpenTelemetrySdk.class); + assertThat(context).doesNotHaveBean(Resource.class); + }); + } + + @Test + void backsOffOnUserSuppliedBeans() { + this.runner.withUserConfiguration(UserConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OpenTelemetry.class); + assertThat(context).hasBean("customOpenTelemetry"); + assertThat(context).hasSingleBean(Resource.class); + assertThat(context).hasBean("customResource"); + }); + } + + @Test + void shouldApplySpringApplicationNameToResource() { + this.runner.withPropertyValues("spring.application.name=my-application").run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()) + .contains(entry(ResourceAttributes.SERVICE_NAME, "my-application")); + }); + } + + @Test + void shouldFallbackToDefaultApplicationNameIfSpringApplicationNameIsNotSet() { + this.runner.run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()) + .contains(entry(ResourceAttributes.SERVICE_NAME, "application")); + }); + } + + @Test + void shouldApplyResourceAttributesFromProperties() { + this.runner.withPropertyValues("management.opentelemetry.resource-attributes.region=us-west").run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()).contains(entry(AttributeKey.stringKey("region"), "us-west")); + }); + } + + @Test + void shouldRegisterSdkTracerProviderIfAvailable() { + this.runner.withBean(SdkTracerProvider.class, () -> SdkTracerProvider.builder().build()).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getTracerProvider()).isNotNull(); + }); + } + + @Test + void shouldRegisterContextPropagatorsIfAvailable() { + this.runner.withBean(ContextPropagators.class, ContextPropagators::noop).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getPropagators()).isNotNull(); + }); + } + + @Test + void shouldRegisterSdkLoggerProviderIfAvailable() { + this.runner.withBean(SdkLoggerProvider.class, () -> SdkLoggerProvider.builder().build()).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getLogsBridge()).isNotNull(); + }); + } + + @Test + void shouldRegisterSdkMeterProviderIfAvailable() { + this.runner.withBean(SdkMeterProvider.class, () -> SdkMeterProvider.builder().build()).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getMeterProvider()).isNotNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + private static class UserConfiguration { + + @Bean + OpenTelemetry customOpenTelemetry() { + return mock(OpenTelemetry.class); + } + + @Bean + Resource customResource() { + return Resource.getDefault(); + } + + } + +} 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 index 6a243e9228..77c997cd19 100644 --- 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 @@ -29,6 +29,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.MDC; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; @@ -151,8 +152,9 @@ class BaggagePropagationIntegrationTests { OTEL_DEFAULT { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class)) .withPropertyValues("management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", "management.tracing.baggage.correlation.fields=country-code,bp"); } @@ -172,8 +174,9 @@ class BaggagePropagationIntegrationTests { OTEL_W3C { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class)) .withPropertyValues("management.tracing.propagation.type=W3C", "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", "management.tracing.baggage.correlation.fields=country-code,bp"); @@ -205,8 +208,9 @@ class BaggagePropagationIntegrationTests { OTEL_B3 { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.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"); @@ -216,8 +220,9 @@ class BaggagePropagationIntegrationTests { OTEL_B3_MULTI { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class)) .withPropertyValues("management.tracing.propagation.type=B3_MULTI", "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/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index f7c94ddffe..6ff9bda7d2 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 @@ -35,7 +35,6 @@ 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.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.MeterProvider; @@ -83,7 +82,9 @@ import static org.mockito.Mockito.mock; class OpenTelemetryAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of( + org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class, + OpenTelemetryAutoConfiguration.class)); @Test void shouldSupplyBeans() { @@ -91,7 +92,6 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).hasSingleBean(OtelTracer.class); assertThat(context).hasSingleBean(EventPublisher.class); assertThat(context).hasSingleBean(OtelCurrentTraceContext.class); - assertThat(context).hasSingleBean(OpenTelemetry.class); assertThat(context).hasSingleBean(SdkTracerProvider.class); assertThat(context).hasSingleBean(ContextPropagators.class); assertThat(context).hasSingleBean(Sampler.class); @@ -123,7 +123,6 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).doesNotHaveBean(OtelTracer.class); assertThat(context).doesNotHaveBean(EventPublisher.class); assertThat(context).doesNotHaveBean(OtelCurrentTraceContext.class); - assertThat(context).doesNotHaveBean(OpenTelemetry.class); assertThat(context).doesNotHaveBean(SdkTracerProvider.class); assertThat(context).doesNotHaveBean(ContextPropagators.class); assertThat(context).doesNotHaveBean(Sampler.class); @@ -148,8 +147,6 @@ class OpenTelemetryAutoConfigurationTests { assertThat(context).hasSingleBean(EventPublisher.class); assertThat(context).hasBean("customOtelCurrentTraceContext"); assertThat(context).hasSingleBean(OtelCurrentTraceContext.class); - assertThat(context).hasBean("customOpenTelemetry"); - assertThat(context).hasSingleBean(OpenTelemetry.class); assertThat(context).hasBean("customSdkTracerProvider"); assertThat(context).hasSingleBean(SdkTracerProvider.class); assertThat(context).hasBean("customContextPropagators"); @@ -369,11 +366,6 @@ class OpenTelemetryAutoConfigurationTests { return mock(OtelCurrentTraceContext.class); } - @Bean - OpenTelemetry customOpenTelemetry() { - return mock(OpenTelemetry.class); - } - @Bean SdkTracerProvider customSdkTracerProvider() { return SdkTracerProvider.builder().build(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java index 47fbcc824a..0fcc77811f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java @@ -34,8 +34,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -50,9 +50,10 @@ class OtlpAutoConfigurationIntegrationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("management.tracing.sampling.probability=1.0") - .withConfiguration( - AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class, - OpenTelemetryAutoConfiguration.class, OtlpAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, + MicrometerTracingAutoConfiguration.class, OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class, + OtlpAutoConfiguration.class)); private final MockWebServer mockWebServer = new MockWebServer(); diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 742e83f92e..53c79996f7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -67,4 +67,15 @@ include::code:MyObservationPredicate[] The preceding example will prevent all observations whose name contains "denied". +[[actuator.observability.opentelemetry]] +=== OpenTelemetry Support +Spring Boot's actuator module includes basic support for https://opentelemetry.io/[OpenTelemetry]. + +It provides a bean of type `OpenTelemetry`, and if there are beans of type `SdkTracerProvider`, `ContextPropagators`, `SdkLoggerProvider` or `SdkMeterProvider` in the application context, they automatically get registered. +Additionally, it provides a `Resource` bean. +The attributes of the `Resource` can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. + +NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. +OpenTelemetry tracing is only auto-configured when used together with <>. + The next sections will provide more details about logging, metrics and traces.