diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/SemanticConventionAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/SemanticConventionAutoConfiguration.java new file mode 100644 index 000000000000..74968e27ab59 --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/SemanticConventionAutoConfiguration.java @@ -0,0 +1,115 @@ +/* + * 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.micrometer.metrics.autoconfigure; + +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.jvm.convention.JvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmCpuMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmThreadMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmCpuMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmThreadMeterConventions; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.micrometer.metrics.autoconfigure.jvm.JvmMetricsAutoConfiguration; +import org.springframework.boot.micrometer.metrics.autoconfigure.system.SystemMetricsAutoConfiguration; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for semantic conventions for metrics + * and observations. + * + * @since 4.1.0 + */ +@AutoConfiguration(before = { JvmMetricsAutoConfiguration.class, SystemMetricsAutoConfiguration.class }) +@EnableConfigurationProperties(ObservationProperties.class) +public final class SemanticConventionAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "management.observations", name = "conventions", havingValue = "micrometer", + matchIfMissing = true) + static class MicrometerSemanticConventionConfiguration { + + @Bean + @ConditionalOnMissingBean(JvmMemoryMeterConventions.class) + MicrometerJvmMemoryMeterConventions micrometerJvmMemoryMeterConventions() { + return new MicrometerJvmMemoryMeterConventions(); + } + + @Bean + @ConditionalOnMissingBean(JvmClassLoadingMeterConventions.class) + MicrometerJvmClassLoadingMeterConventions micrometerJvmClassLoadingMeterConventions() { + return new MicrometerJvmClassLoadingMeterConventions(); + } + + @Bean + @ConditionalOnMissingBean(JvmCpuMeterConventions.class) + MicrometerJvmCpuMeterConventions micrometerJvmCpuMeterConventions() { + return new MicrometerJvmCpuMeterConventions(Tags.empty()); + } + + @Bean + @ConditionalOnMissingBean(JvmThreadMeterConventions.class) + MicrometerJvmThreadMeterConventions micrometerJvmThreadMeterConventions() { + return new MicrometerJvmThreadMeterConventions(Tags.empty()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = "management.observations", name = "conventions", havingValue = "opentelemetry") + static class OpenTelemetrySemanticConventionConfiguration { + + @Bean + @ConditionalOnMissingBean(JvmMemoryMeterConventions.class) + OpenTelemetryJvmMemoryMeterConventions openTelemetryJvmMemoryMeterConventions() { + return new OpenTelemetryJvmMemoryMeterConventions(Tags.empty()); + } + + @Bean + @ConditionalOnMissingBean(JvmClassLoadingMeterConventions.class) + OpenTelemetryJvmClassLoadingMeterConventions openTelemetryJvmClassLoadingMeterConventions() { + return new OpenTelemetryJvmClassLoadingMeterConventions(); + } + + @Bean + @ConditionalOnMissingBean(JvmCpuMeterConventions.class) + OpenTelemetryJvmCpuMeterConventions openTelemetryJvmCpuMeterConventions() { + return new OpenTelemetryJvmCpuMeterConventions(Tags.empty()); + } + + @Bean + @ConditionalOnMissingBean(JvmThreadMeterConventions.class) + OpenTelemetryJvmThreadMeterConventions openTelemetryJvmThreadMeterConventions() { + return new OpenTelemetryJvmThreadMeterConventions(Tags.empty()); + } + + } + +} diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/SemanticConventionAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/SemanticConventionAutoConfigurationTests.java new file mode 100644 index 000000000000..ac9cb59c840e --- /dev/null +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/SemanticConventionAutoConfigurationTests.java @@ -0,0 +1,158 @@ +/* + * 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.micrometer.metrics.autoconfigure; + +import java.lang.management.MemoryPoolMXBean; + +import io.micrometer.core.instrument.binder.MeterConvention; +import io.micrometer.core.instrument.binder.SimpleMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.JvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmCpuMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmThreadMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmClassLoadingMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmCpuMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmThreadMeterConventions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +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; + +/** + * Tests for {@link SemanticConventionAutoConfiguration}. + */ +class SemanticConventionAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SemanticConventionAutoConfiguration.class)); + + @Test + void registersMicrometerConventionsByDefault() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(MicrometerJvmMemoryMeterConventions.class); + assertThat(context).hasSingleBean(MicrometerJvmClassLoadingMeterConventions.class); + assertThat(context).hasSingleBean(MicrometerJvmCpuMeterConventions.class); + assertThat(context).hasSingleBean(MicrometerJvmThreadMeterConventions.class); + assertThat(context).doesNotHaveBean(OpenTelemetryJvmMemoryMeterConventions.class); + assertThat(context).doesNotHaveBean(OpenTelemetryJvmClassLoadingMeterConventions.class); + assertThat(context).doesNotHaveBean(OpenTelemetryJvmCpuMeterConventions.class); + assertThat(context).doesNotHaveBean(OpenTelemetryJvmThreadMeterConventions.class); + }); + } + + @Test + void registersOpenTelemetryConventionsWhenConventionsSetToOpenTelemetry() { + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry").run((context) -> { + assertThat(context).hasSingleBean(JvmMemoryMeterConventions.class) + .hasSingleBean(OpenTelemetryJvmMemoryMeterConventions.class); + assertThat(context).hasSingleBean(JvmClassLoadingMeterConventions.class) + .hasSingleBean(OpenTelemetryJvmClassLoadingMeterConventions.class); + assertThat(context).hasSingleBean(JvmCpuMeterConventions.class) + .hasSingleBean(OpenTelemetryJvmCpuMeterConventions.class); + assertThat(context).hasSingleBean(JvmThreadMeterConventions.class) + .hasSingleBean(OpenTelemetryJvmThreadMeterConventions.class); + }); + } + + @Test + void allowsCustomMicrometerConventionsToBeUsed() { + this.contextRunner.withPropertyValues("management.observations.conventions=micrometer") + .withUserConfiguration(CustomJvmMemoryMeterConventionsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(JvmMemoryMeterConventions.class) + .hasBean("customJvmMemoryMeterConventions"); + assertThat(context).doesNotHaveBean(MicrometerJvmMemoryMeterConventions.class); + assertThat(context).hasSingleBean(MicrometerJvmClassLoadingMeterConventions.class); + assertThat(context).hasSingleBean(MicrometerJvmCpuMeterConventions.class); + assertThat(context).hasSingleBean(MicrometerJvmThreadMeterConventions.class); + }); + } + + @Test + void allowsCustomOpenTelemetryConventionsToBeUsed() { + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry") + .withUserConfiguration(CustomJvmClassLoadingMeterConventionsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(JvmClassLoadingMeterConventions.class) + .hasBean("customJvmClassLoadingMeterConventions"); + assertThat(context).doesNotHaveBean(MicrometerJvmClassLoadingMeterConventions.class); + assertThat(context).hasSingleBean(OpenTelemetryJvmMemoryMeterConventions.class); + assertThat(context).hasSingleBean(OpenTelemetryJvmCpuMeterConventions.class); + assertThat(context).hasSingleBean(OpenTelemetryJvmThreadMeterConventions.class); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomJvmMemoryMeterConventionsConfiguration { + + @Bean + JvmMemoryMeterConventions customJvmMemoryMeterConventions() { + return new JvmMemoryMeterConventions() { + @Override + public MeterConvention getMemoryUsedConvention() { + return new SimpleMeterConvention<>("my.memory.used"); + } + + @Override + public MeterConvention getMemoryCommittedConvention() { + return new SimpleMeterConvention<>("my.memory.committed"); + } + + @Override + public MeterConvention getMemoryMaxConvention() { + return new SimpleMeterConvention<>("my.memory.max"); + } + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomJvmClassLoadingMeterConventionsConfiguration { + + @Bean + JvmClassLoadingMeterConventions customJvmClassLoadingMeterConventions() { + return new JvmClassLoadingMeterConventions() { + @Override + public MeterConvention loadedConvention() { + return new SimpleMeterConvention<>("my.classes.loaded"); + } + + @Override + public MeterConvention unloadedConvention() { + return new SimpleMeterConvention<>("my.classes.unloaded"); + } + + @Override + public MeterConvention currentClassCountConvention() { + return new SimpleMeterConvention<>("my.classes.current"); + } + }; + } + + } + +} diff --git a/module/spring-boot-micrometer-observation/src/main/java/org/springframework/boot/micrometer/observation/autoconfigure/ObservationProperties.java b/module/spring-boot-micrometer-observation/src/main/java/org/springframework/boot/micrometer/observation/autoconfigure/ObservationProperties.java index 50081216692f..7fc41de311bb 100644 --- a/module/spring-boot-micrometer-observation/src/main/java/org/springframework/boot/micrometer/observation/autoconfigure/ObservationProperties.java +++ b/module/spring-boot-micrometer-observation/src/main/java/org/springframework/boot/micrometer/observation/autoconfigure/ObservationProperties.java @@ -45,6 +45,8 @@ public class ObservationProperties { */ private Map enable = new LinkedHashMap<>(); + private ConventionsVariant conventions = ConventionsVariant.MICROMETER; + public Map getEnable() { return this.enable; } @@ -65,6 +67,20 @@ public void setKeyValues(Map keyValues) { this.keyValues = keyValues; } + public ConventionsVariant getConventions() { + return this.conventions; + } + + public void setConventions(ConventionsVariant conventions) { + this.conventions = conventions; + } + + public enum ConventionsVariant { + + OPENTELEMETRY, MICROMETER, + + } + public static class Http { private final Client client = new Client(); diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfiguration.java index 492a80d756db..1e9101896abc 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfiguration.java @@ -21,12 +21,13 @@ import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.DispatcherType; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -39,6 +40,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.observation.OpenTelemetryServerRequestObservationConvention; import org.springframework.http.server.observation.ServerRequestObservationConvention; import org.springframework.web.filter.ServerHttpObservationFilter; import org.springframework.web.servlet.DispatcherServlet; @@ -64,13 +66,26 @@ public final class WebMvcObservationAutoConfiguration { @Bean - @ConditionalOnMissingFilterBean - FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry, - ObjectProvider customConvention, + @ConditionalOnMissingBean(ServerRequestObservationConvention.class) + @ConditionalOnProperty(name = "management.observations.conventions", havingValue = "micrometer", + matchIfMissing = true) + DefaultServerRequestObservationConvention micrometerServerRequestObservationConvention( ObservationProperties observationProperties) { String name = observationProperties.getHttp().getServer().getRequests().getName(); - ServerRequestObservationConvention convention = customConvention - .getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); + return new DefaultServerRequestObservationConvention(name); + } + + @Bean + @ConditionalOnMissingBean(ServerRequestObservationConvention.class) + @ConditionalOnProperty(name = "management.observations.conventions", havingValue = "opentelemetry") + OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObservationConvention() { + return new OpenTelemetryServerRequestObservationConvention(); + } + + @Bean + @ConditionalOnMissingFilterBean + FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry, + ServerRequestObservationConvention convention) { ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfigurationTests.java index 2648e2f74491..ca1a492488e8 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcObservationAutoConfigurationTests.java @@ -46,6 +46,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.observation.OpenTelemetryServerRequestObservationConvention; import org.springframework.test.web.servlet.assertj.MockMvcTester; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -93,6 +94,20 @@ void definesFilterWhenRegistryIsPresent() { }); } + @Test + void defaultMicrometerConvention() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(DefaultServerRequestObservationConvention.class); + }); + } + + @Test + void openTelemetryConventionConfiguredViaProperties() { + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry").run((context) -> { + assertThat(context).hasSingleBean(OpenTelemetryServerRequestObservationConvention.class); + }); + } + @Test void customConventionWhenPresent() { this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class)