diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java index 3e6dedaca3aa..8a2170d135a7 100644 --- a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/OpenTelemetrySdkAutoConfiguration.java @@ -20,6 +20,7 @@ import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.logs.LogRecordProcessor; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; @@ -44,6 +45,7 @@ * {@link EnableAutoConfiguration Auto-configuration} for the OpenTelemetry SDK. * * @author Moritz Halbritter + * @author Thomas Vitale * @since 4.0.0 */ @AutoConfiguration @@ -68,6 +70,12 @@ OpenTelemetrySdk openTelemetrySdk(ObjectProvider openTelemetr return builder.build(); } + @Bean + @ConditionalOnMissingBean + Clock clock() { + return Clock.getDefault(); + } + @Bean @ConditionalOnMissingBean Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsAutoConfiguration.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsAutoConfiguration.java new file mode 100644 index 000000000000..b455a43c70fa --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsAutoConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-present 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.opentelemetry.autoconfigure.metrics; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.common.Clock; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.export.CardinalityLimitSelector; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; +import io.opentelemetry.sdk.resources.Resource; + +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; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry Metrics. + * + * @author Thomas Vitale + * @since 4.0.0 + */ +@AutoConfiguration +@ConditionalOnClass(SdkMeterProvider.class) +@EnableConfigurationProperties(OpenTelemetryMetricsProperties.class) +public final class OpenTelemetryMetricsAutoConfiguration { + + static final String INSTRUMENTATION_SCOPE_NAME = "org.springframework.boot"; + + @Bean + @ConditionalOnMissingBean + SdkMeterProvider meterProvider(Clock clock, ExemplarFilter exemplarFilter, + OpenTelemetryMetricsProperties properties, Resource resource, + ObjectProvider customizers) { + SdkMeterProviderBuilder builder = SdkMeterProvider.builder().setClock(clock).setResource(resource); + if (properties.getExemplars().isEnabled()) { + SdkMeterProviderUtil.setExemplarFilter(builder, exemplarFilter); + } + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + CardinalityLimitSelector cardinalityLimitSelector(OpenTelemetryMetricsProperties properties) { + return (instrumentType) -> properties.getCardinalityLimit(); + } + + @Bean + @ConditionalOnMissingBean + ExemplarFilter exemplarFilter(OpenTelemetryMetricsProperties properties) { + return switch (properties.getExemplars().getFilter()) { + case ALWAYS_ON -> ExemplarFilter.alwaysOn(); + case ALWAYS_OFF -> ExemplarFilter.alwaysOff(); + case TRACE_BASED -> ExemplarFilter.traceBased(); + }; + } + + @Bean + @ConditionalOnMissingBean + Meter meter(OpenTelemetry openTelemetry) { + return openTelemetry.getMeter(INSTRUMENTATION_SCOPE_NAME); + } + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsProperties.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsProperties.java new file mode 100644 index 000000000000..048c7cf24673 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsProperties.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-present 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.opentelemetry.autoconfigure.metrics; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for OpenTelemetry Metrics. + * + * @author Thomas Vitale + * @since 4.0.0 + */ +@ConfigurationProperties("management.opentelemetry.metrics") +public class OpenTelemetryMetricsProperties { + + /** + * Configuration for exemplars. + */ + private final Exemplars exemplars = new Exemplars(); + + /** + * Maximum number of distinct points per metric. + */ + private int cardinalityLimit = 2000; + + public Exemplars getExemplars() { + return this.exemplars; + } + + public int getCardinalityLimit() { + return this.cardinalityLimit; + } + + public void setCardinalityLimit(int cardinalityLimit) { + this.cardinalityLimit = cardinalityLimit; + } + + /** + * Configuration properties for exemplars. + */ + public static class Exemplars { + + /** + * Whether exemplars should be enabled. + */ + private boolean enabled = false; + + /** + * Determines which measurements are eligible to become Exemplars. + */ + private ExemplarFilter filter = ExemplarFilter.TRACE_BASED; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public ExemplarFilter getFilter() { + return this.filter; + } + + public void setFilter(ExemplarFilter filter) { + this.filter = filter; + } + + } + + /** + * Filter for which measurements are eligible to become Exemplars. + */ + public enum ExemplarFilter { + + /** + * Filter which makes all measurements eligible for being an exemplar. + */ + ALWAYS_ON, + + /** + * Filter which makes no measurements eligible for being an exemplar. + */ + ALWAYS_OFF, + + /** + * Filter that only accepts measurements where there is a span in context that is + * being sampled. + */ + TRACE_BASED + + } + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/SdkMeterProviderBuilderCustomizer.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/SdkMeterProviderBuilderCustomizer.java new file mode 100644 index 000000000000..e91f91a93fcf --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/SdkMeterProviderBuilderCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present 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.opentelemetry.autoconfigure.metrics; + +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; + +/** + * Callback that can be used to customize the {@link SdkMeterProviderBuilder} used to + * build the auto-configured {@link SdkMeterProvider}. + * + * @author Thomas Vitale + * @since 4.0.0 + */ +@FunctionalInterface +public interface SdkMeterProviderBuilderCustomizer { + + /** + * Customize the given {@code builder}. + * @param builder the builder to customize + */ + void customize(SdkMeterProviderBuilder builder); + +} diff --git a/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/package-info.java b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/package-info.java new file mode 100644 index 000000000000..b8edb3ad42b2 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/main/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-present 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 Metrics. + */ +package org.springframework.boot.opentelemetry.autoconfigure.metrics; diff --git a/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index f94012057d25..2a49c21ed171 100644 --- a/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/module/spring-boot-opentelemetry/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration org.springframework.boot.opentelemetry.autoconfigure.logging.OpenTelemetryLoggingExportAutoConfiguration +org.springframework.boot.opentelemetry.autoconfigure.metrics.OpenTelemetryMetricsAutoConfiguration diff --git a/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsAutoConfigurationTests.java b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsAutoConfigurationTests.java new file mode 100644 index 000000000000..84bed74d9545 --- /dev/null +++ b/module/spring-boot-opentelemetry/src/test/java/org/springframework/boot/opentelemetry/autoconfigure/metrics/OpenTelemetryMetricsAutoConfigurationTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-present 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.opentelemetry.autoconfigure.metrics; + +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.CardinalityLimitSelector; +import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration; +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; + +/** + * Unit tests for {@link OpenTelemetryMetricsAutoConfiguration}. + * + * @author Thomas Vitale + */ +class OpenTelemetryMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class, + OpenTelemetryMetricsAutoConfiguration.class)); + + @Test + void whenSdkMeterProviderIsNotOnClasspathDoesNotProvideBeans() { + this.contextRunner.withClassLoader(new FilteredClassLoader(SdkMeterProvider.class)).run((context) -> { + assertThat(context).doesNotHaveBean(SdkMeterProvider.class); + assertThat(context).doesNotHaveBean(CardinalityLimitSelector.class); + assertThat(context).doesNotHaveBean(ExemplarFilter.class); + assertThat(context).doesNotHaveBean(Meter.class); + }); + } + + @Test + void meterProviderAvailableWithDefaultConfiguration() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(SdkMeterProvider.class); + assertThat(context).hasSingleBean(CardinalityLimitSelector.class); + assertThat(context).hasSingleBean(ExemplarFilter.class); + assertThat(context).hasSingleBean(Meter.class); + }); + } + + @Test + void cardinalityLimitSelectorConfigurationApplied() { + this.contextRunner.withPropertyValues("management.opentelemetry.metrics.cardinality-limit=200") + .run((context) -> { + CardinalityLimitSelector cardinalityLimitSelector = context.getBean(CardinalityLimitSelector.class); + assertThat(cardinalityLimitSelector.getCardinalityLimit(InstrumentType.COUNTER)).isEqualTo(200); + }); + } + + @Test + void exemplarFilterConfigurationApplied() { + this.contextRunner.withPropertyValues("management.opentelemetry.metrics.exemplars.filter=always-on") + .run((context) -> { + ExemplarFilter exemplarFilter = context.getBean(ExemplarFilter.class); + assertThat(exemplarFilter).isEqualTo(ExemplarFilter.alwaysOn()); + }); + + this.contextRunner.withPropertyValues("management.opentelemetry.metrics.exemplars.filter=always-off") + .run((context) -> { + ExemplarFilter exemplarFilter = context.getBean(ExemplarFilter.class); + assertThat(exemplarFilter).isEqualTo(ExemplarFilter.alwaysOff()); + }); + + this.contextRunner.withPropertyValues("management.opentelemetry.metrics.exemplars.filter=trace-based") + .run((context) -> { + ExemplarFilter exemplarFilter = context.getBean(ExemplarFilter.class); + assertThat(exemplarFilter).isEqualTo(ExemplarFilter.traceBased()); + }); + } + + @Test + void customCardinalityLimitSelectorAvailable() { + this.contextRunner.withUserConfiguration(CustomCardinalityLimitSelectorConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CardinalityLimitSelector.class); + assertThat(context.getBean(CardinalityLimitSelector.class)) + .isSameAs(context.getBean(CustomCardinalityLimitSelectorConfiguration.class) + .customCardinalityLimitSelector()); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomCardinalityLimitSelectorConfiguration { + + private final CardinalityLimitSelector customCardinalityLimitSelector = mock(CardinalityLimitSelector.class); + + @Bean + CardinalityLimitSelector customCardinalityLimitSelector() { + return this.customCardinalityLimitSelector; + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomMeterBuilderCustomizerConfiguration { + + private final SdkMeterProviderBuilderCustomizer customMeterBuilderCustomizer = mock( + SdkMeterProviderBuilderCustomizer.class); + + @Bean + SdkMeterProviderBuilderCustomizer customMetricBuilderCustomizer() { + return this.customMeterBuilderCustomizer; + } + + } + +}