From 7d43f31732f3b9c834ad0adf6d78e1a59381c4c9 Mon Sep 17 00:00:00 2001 From: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> Date: Fri, 13 Feb 2026 19:12:30 +0900 Subject: [PATCH] Configure OpenTelemetry semantic conventions Adds a management.observations.conventions property to control which set of conventions are auto-configured across the application. Configures WebMvc and WebFlux ServerRequestObservationConvention beans to switch to OpenTelemetry conventions when configured. Configures JVM and system meter binders using their builder APIs to apply OpenTelemetry conventions while still allowing individual meter conventions to be overridden via beans. Signed-off-by: Tommy Ludwig <8924140+shakuzen@users.noreply.github.com> See gh-49241 --- .../jvm/JvmMetricsAutoConfiguration.java | 50 ++++++++---- .../SystemMetricsAutoConfiguration.java | 16 ++-- .../jvm/JvmMetricsAutoConfigurationTests.java | 77 +++++++++++++++++++ .../SystemMetricsAutoConfigurationTests.java | 32 ++++++++ .../autoconfigure/ObservationProperties.java | 19 +++++ .../WebFluxObservationAutoConfiguration.java | 13 +++- ...FluxObservationAutoConfigurationTests.java | 7 ++ .../WebMvcObservationAutoConfiguration.java | 27 +++++-- ...bMvcObservationAutoConfigurationTests.java | 13 ++++ 9 files changed, 228 insertions(+), 26 deletions(-) diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java index 35f5a69c023..48b3e9cdd99 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfiguration.java @@ -34,6 +34,7 @@ import io.micrometer.core.instrument.binder.jvm.convention.JvmClassUnloadedMeter import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryCommittedMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMaxMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryUsedAfterLastGcMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryUsedMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadCountMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadMeterConventions; @@ -49,9 +50,12 @@ 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.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationProperties; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationProperties.ConventionsVariant; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; @@ -67,6 +71,7 @@ import org.springframework.util.ClassUtils; @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class) +@EnableConfigurationProperties(ObservationProperties.class) public final class JvmMetricsAutoConfiguration { private static final String VIRTUAL_THREAD_METRICS_CLASS = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics"; @@ -86,26 +91,29 @@ public final class JvmMetricsAutoConfiguration { @Bean @ConditionalOnMissingBean @SuppressWarnings("deprecation") - JvmMemoryMetrics jvmMemoryMetrics(ObjectProvider jvmMemoryMeterConventions, + JvmMemoryMetrics jvmMemoryMetrics(ObservationProperties observationProperties, + ObjectProvider jvmMemoryMeterConventions, ObjectProvider jvmMemoryUsedMeterConvention, ObjectProvider jvmMemoryCommittedMeterConvention, - ObjectProvider jvmMemoryMaxMeterConvention) { + ObjectProvider jvmMemoryMaxMeterConvention, + ObjectProvider jvmMemoryUsedAfterLastGcMeterConvention) { JvmMemoryMetricsFactory factory = new JvmMemoryMetricsFactory(jvmMemoryUsedMeterConvention.getIfAvailable(), - jvmMemoryCommittedMeterConvention.getIfAvailable(), jvmMemoryMaxMeterConvention.getIfAvailable()); + jvmMemoryCommittedMeterConvention.getIfAvailable(), jvmMemoryMaxMeterConvention.getIfAvailable(), + jvmMemoryUsedAfterLastGcMeterConvention.getIfAvailable()); JvmMemoryMeterConventions deprecatedConventions = jvmMemoryMeterConventions.getIfAvailable(); if (deprecatedConventions != null && factory.hasConvention()) { throw new IllegalStateException("Either %s or the interfaces that supersede it should be set" .formatted(JvmMemoryMeterConventions.class.getSimpleName())); } return (deprecatedConventions != null) ? new JvmMemoryMetrics(Collections.emptyList(), deprecatedConventions) - : factory.create(); - + : factory.create(observationProperties); } @Bean @ConditionalOnMissingBean @SuppressWarnings("deprecation") - JvmThreadMetrics jvmThreadMetrics(ObjectProvider jvmThreadMeterConventions, + JvmThreadMetrics jvmThreadMetrics(ObservationProperties observationProperties, + ObjectProvider jvmThreadMeterConventions, ObjectProvider jvmThreadCountMeterConvention) { JvmThreadMeterConventions deprecatedConventions = jvmThreadMeterConventions.getIfAvailable(); JvmThreadCountMeterConvention convention = jvmThreadCountMeterConvention.getIfAvailable(); @@ -117,14 +125,19 @@ public final class JvmMetricsAutoConfiguration { if (deprecatedConventions != null) { return new JvmThreadMetrics(Collections.emptyList(), deprecatedConventions); } - return (convention != null) ? JvmThreadMetrics.builder().threadCountConvention(convention).build() - : new JvmThreadMetrics(); + JvmThreadMetrics.Builder builder = JvmThreadMetrics.builder(); + if (observationProperties.getConventions() == ConventionsVariant.OPENTELEMETRY) { + builder.openTelemetryConventions(); + } + PropertyMapper map = PropertyMapper.get(); + map.from(convention).to(builder::threadCountConvention); + return builder.build(); } @Bean @ConditionalOnMissingBean @SuppressWarnings("deprecation") - ClassLoaderMetrics classLoaderMetrics( + ClassLoaderMetrics classLoaderMetrics(ObservationProperties observationProperties, ObjectProvider jvmClassLoadingMeterConventions, ObjectProvider jvmClassCountMeterConvention, ObjectProvider jvmClassLoadedMeterConvention, @@ -136,7 +149,8 @@ public final class JvmMetricsAutoConfiguration { throw new IllegalStateException("Either %s or the interfaces that supersede it should be set" .formatted(JvmClassLoadingMeterConventions.class.getSimpleName())); } - return (deprecatedConventions != null) ? new ClassLoaderMetrics(deprecatedConventions) : factory.create(); + return (deprecatedConventions != null) ? new ClassLoaderMetrics(deprecatedConventions) + : factory.create(observationProperties); } @Bean @@ -168,19 +182,24 @@ public final class JvmMetricsAutoConfiguration { private record JvmMemoryMetricsFactory(@Nullable JvmMemoryUsedMeterConvention memoryUsedConvention, @Nullable JvmMemoryCommittedMeterConvention memoryCommittedConvention, - @Nullable JvmMemoryMaxMeterConvention memoryMaxConvention) { + @Nullable JvmMemoryMaxMeterConvention memoryMaxConvention, + @Nullable JvmMemoryUsedAfterLastGcMeterConvention memoryUsedAfterLastGcConvention) { boolean hasConvention() { return this.memoryUsedConvention != null || this.memoryCommittedConvention != null - || this.memoryMaxConvention != null; + || this.memoryMaxConvention != null || this.memoryUsedAfterLastGcConvention != null; } - JvmMemoryMetrics create() { + JvmMemoryMetrics create(ObservationProperties observationProperties) { JvmMemoryMetrics.Builder builder = JvmMemoryMetrics.builder(); + if (observationProperties.getConventions() == ConventionsVariant.OPENTELEMETRY) { + builder.openTelemetryConventions(); + } PropertyMapper map = PropertyMapper.get(); map.from(this.memoryUsedConvention).to(builder::memoryUsedConvention); map.from(this.memoryCommittedConvention).to(builder::memoryCommittedConvention); map.from(this.memoryMaxConvention).to(builder::memoryMaxConvention); + map.from(this.memoryUsedAfterLastGcConvention).to(builder::memoryUsedAfterLastGcConvention); return builder.build(); } @@ -195,8 +214,11 @@ public final class JvmMetricsAutoConfiguration { || this.classUnloadedConvention != null; } - ClassLoaderMetrics create() { + ClassLoaderMetrics create(ObservationProperties observationProperties) { ClassLoaderMetrics.Builder builder = ClassLoaderMetrics.builder(); + if (observationProperties.getConventions() == ConventionsVariant.OPENTELEMETRY) { + builder.openTelemetryConventions(); + } PropertyMapper map = PropertyMapper.get(); map.from(this.classCountConvention).to(builder::classCountConvention); map.from(this.classLoadedConvention).to(builder::classLoadedConvention); diff --git a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java index e61cb4156dc..d3077638f53 100644 --- a/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java +++ b/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfiguration.java @@ -43,6 +43,8 @@ import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterR import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsProperties; import org.springframework.boot.micrometer.metrics.system.DiskSpaceMetricsBinder; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationProperties; +import org.springframework.boot.micrometer.observation.autoconfigure.ObservationProperties.ConventionsVariant; import org.springframework.context.annotation.Bean; /** @@ -55,7 +57,7 @@ import org.springframework.context.annotation.Bean; @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class) -@EnableConfigurationProperties(MetricsProperties.class) +@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) public final class SystemMetricsAutoConfiguration { @Bean @@ -67,7 +69,8 @@ public final class SystemMetricsAutoConfiguration { @Bean @ConditionalOnMissingBean @SuppressWarnings("deprecation") - ProcessorMetrics processorMetrics(ObjectProvider jvmCpuMeterConventions, + ProcessorMetrics processorMetrics(ObservationProperties observationProperties, + ObjectProvider jvmCpuMeterConventions, ObjectProvider jvmCpuTimeMeterConvention, ObjectProvider jvmCpuCountMeterConvention, ObjectProvider jvmCpuLoadMeterConvention) { @@ -79,7 +82,7 @@ public final class SystemMetricsAutoConfiguration { .formatted(JvmCpuMeterConventions.class.getSimpleName())); } return (deprecatedConventions != null) ? new ProcessorMetrics(Collections.emptyList(), deprecatedConventions) - : factory.create(); + : factory.create(observationProperties); } @Bean @@ -103,9 +106,12 @@ public final class SystemMetricsAutoConfiguration { return this.cpuTimeConvention != null || this.cpuCountConvention != null || this.cpuLoadConvention != null; } - ProcessorMetrics create() { - PropertyMapper map = PropertyMapper.get(); + ProcessorMetrics create(ObservationProperties observationProperties) { ProcessorMetrics.Builder builder = ProcessorMetrics.builder(); + if (observationProperties.getConventions() == ConventionsVariant.OPENTELEMETRY) { + builder.openTelemetryConventions(); + } + PropertyMapper map = PropertyMapper.get(); map.from(this.cpuTimeConvention).to(builder::cpuTimeConvention); map.from(this.cpuCountConvention).to(builder::cpuCountConvention); map.from(this.cpuLoadConvention).to(builder::cpuLoadConvention); diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java index 252b946a5c7..000cd776073 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/jvm/JvmMetricsAutoConfigurationTests.java @@ -33,12 +33,22 @@ import io.micrometer.core.instrument.binder.jvm.convention.JvmClassUnloadedMeter import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryCommittedMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMaxMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryUsedAfterLastGcMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryUsedMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadCountMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmClassCountMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmClassLoadingMeterConventions; import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmMemoryMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmMemoryUsedAfterLastGcMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmMemoryUsedMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmThreadCountMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmThreadMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmClassCountMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmMemoryCommittedMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmMemoryUsedAfterLastGcMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmMemoryUsedMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmThreadCountMeterConvention; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; @@ -80,6 +90,62 @@ class JvmMetricsAutoConfigurationTests { this.contextRunner.run(assertMetricsBeans()); } + @Test + void autoConfiguresJvmMetricsWithDefaultConventions() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(JvmMemoryMetrics.class) + .getBean(JvmMemoryMetrics.class) + .extracting("memoryUsedConvention") + .isInstanceOf(MicrometerJvmMemoryUsedMeterConvention.class); + assertThat(context.getBean(JvmMemoryMetrics.class)).extracting("memoryUsedAfterLastGcConvention") + .isInstanceOf(MicrometerJvmMemoryUsedAfterLastGcMeterConvention.class); + assertThat(context).hasSingleBean(JvmThreadMetrics.class) + .getBean(JvmThreadMetrics.class) + .extracting("threadCountConvention") + .isInstanceOf(MicrometerJvmThreadCountMeterConvention.class); + assertThat(context).hasSingleBean(ClassLoaderMetrics.class) + .getBean(ClassLoaderMetrics.class) + .extracting("classCountConvention") + .isInstanceOf(MicrometerJvmClassCountMeterConvention.class); + }); + } + + @Test + void autoConfiguresJvmMetricsWithOpenTelemetryConventions() { + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry").run((context) -> { + assertThat(context).hasSingleBean(JvmMemoryMetrics.class) + .getBean(JvmMemoryMetrics.class) + .extracting("memoryUsedConvention") + .isInstanceOf(OpenTelemetryJvmMemoryUsedMeterConvention.class); + assertThat(context.getBean(JvmMemoryMetrics.class)).extracting("memoryUsedAfterLastGcConvention") + .isInstanceOf(OpenTelemetryJvmMemoryUsedAfterLastGcMeterConvention.class); + assertThat(context).hasSingleBean(JvmThreadMetrics.class) + .getBean(JvmThreadMetrics.class) + .extracting("threadCountConvention") + .isInstanceOf(OpenTelemetryJvmThreadCountMeterConvention.class); + assertThat(context).hasSingleBean(ClassLoaderMetrics.class) + .getBean(ClassLoaderMetrics.class) + .extracting("classCountConvention") + .isInstanceOf(OpenTelemetryJvmClassCountMeterConvention.class); + }); + } + + @Test + void allowsIndividualConventionToBeOverriddenWithOpenTelemetryConventions() { + JvmMemoryUsedMeterConvention customUsed = mock(JvmMemoryUsedMeterConvention.class); + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry") + .withBean(JvmMemoryUsedMeterConvention.class, () -> customUsed) + .run((context) -> { + assertThat(context).hasSingleBean(JvmMemoryMetrics.class) + .getBean(JvmMemoryMetrics.class) + .hasFieldOrPropertyWithValue("memoryUsedConvention", customUsed); + assertThat(context.getBean(JvmMemoryMetrics.class)).extracting("memoryCommittedConvention") + .isInstanceOf(OpenTelemetryJvmMemoryCommittedMeterConvention.class); + assertThat(context.getBean(JvmMemoryMetrics.class)).extracting("memoryUsedAfterLastGcConvention") + .isInstanceOf(OpenTelemetryJvmMemoryUsedAfterLastGcMeterConvention.class); + }); + } + @Test void allowsCustomJvmGcMetricsToBeUsed() { this.contextRunner.withUserConfiguration(CustomJvmGcMetricsConfiguration.class) @@ -171,6 +237,17 @@ class JvmMetricsAutoConfigurationTests { .hasFieldOrPropertyWithValue("memoryMaxConvention", memoryMaxConvention)); } + @Test + void allowCustomJvmMemoryUsedAfterLastGcMeterConventionToBeUsed() { + JvmMemoryUsedAfterLastGcMeterConvention memoryUsedAfterLastGcConvention = mock( + JvmMemoryUsedAfterLastGcMeterConvention.class); + this.contextRunner + .withBean(JvmMemoryUsedAfterLastGcMeterConvention.class, () -> memoryUsedAfterLastGcConvention) + .run((context) -> assertThat(context).hasSingleBean(JvmMemoryMetrics.class) + .getBean(JvmMemoryMetrics.class) + .hasFieldOrPropertyWithValue("memoryUsedAfterLastGcConvention", memoryUsedAfterLastGcConvention)); + } + @Test void allowsCustomJvmThreadMetricsToBeUsed() { this.contextRunner.withUserConfiguration(CustomJvmThreadMetricsConfiguration.class) diff --git a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java index 5ed14518d88..95f7ec91e25 100644 --- a/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java +++ b/module/spring-boot-micrometer-metrics/src/test/java/org/springframework/boot/micrometer/metrics/autoconfigure/system/SystemMetricsAutoConfigurationTests.java @@ -25,7 +25,10 @@ import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuCountMeterConve import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuLoadMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuMeterConventions; import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuTimeMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmCpuCountMeterConvention; import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmCpuMeterConventions; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmCpuCountMeterConvention; +import io.micrometer.core.instrument.binder.jvm.convention.otel.OpenTelemetryJvmCpuLoadMeterConvention; import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics; import io.micrometer.core.instrument.binder.system.ProcessorMetrics; import io.micrometer.core.instrument.binder.system.UptimeMetrics; @@ -73,6 +76,35 @@ class SystemMetricsAutoConfigurationTests { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ProcessorMetrics.class)); } + @Test + void autoConfiguresProcessorMetricsWithDefaultConventions() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ProcessorMetrics.class) + .getBean(ProcessorMetrics.class) + .extracting("cpuCountConvention") + .isInstanceOf(MicrometerJvmCpuCountMeterConvention.class)); + } + + @Test + void autoConfiguresProcessorMetricsWithOpenTelemetryConventions() { + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry") + .run((context) -> assertThat(context).hasSingleBean(ProcessorMetrics.class) + .getBean(ProcessorMetrics.class) + .extracting("cpuCountConvention") + .isInstanceOf(OpenTelemetryJvmCpuCountMeterConvention.class)); + } + + @Test + void allowsIndividualConventionToBeOverriddenWithOpenTelemetryConventions() { + JvmCpuCountMeterConvention customCount = mock(JvmCpuCountMeterConvention.class); + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry") + .withBean(JvmCpuCountMeterConvention.class, () -> customCount) + .run((context) -> assertThat(context).hasSingleBean(ProcessorMetrics.class) + .getBean(ProcessorMetrics.class) + .hasFieldOrPropertyWithValue("cpuCountConvention", customCount) + .extracting("cpuLoadConvention") + .isInstanceOf(OpenTelemetryJvmCpuLoadMeterConvention.class)); + } + @Test void allowsCustomProcessorMetricsToBeUsed() { this.contextRunner.withUserConfiguration(CustomProcessorMetricsConfiguration.class) 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 50081216692..167dea29510 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,11 @@ public class ObservationProperties { */ private Map enable = new LinkedHashMap<>(); + /** + * Conventions variant to use when configuring observations and metrics. + */ + private ConventionsVariant conventions = ConventionsVariant.MICROMETER; + public Map getEnable() { return this.enable; } @@ -65,6 +70,20 @@ public class ObservationProperties { 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-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfiguration.java index ae5b9859e74..8fb93657a35 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfiguration.java @@ -25,6 +25,7 @@ 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.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -34,6 +35,7 @@ import org.springframework.boot.micrometer.observation.autoconfigure.Observation import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.reactive.observation.OpenTelemetryServerRequestObservationConvention; import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; /** @@ -71,9 +73,18 @@ public final class WebFluxObservationAutoConfiguration { @Bean @ConditionalOnMissingBean(ServerRequestObservationConvention.class) - DefaultServerRequestObservationConvention defaultServerRequestObservationConvention() { + @ConditionalOnProperty(name = "management.observations.conventions", havingValue = "micrometer", + matchIfMissing = true) + DefaultServerRequestObservationConvention micrometerServerRequestObservationConvention() { return new DefaultServerRequestObservationConvention( this.observationProperties.getHttp().getServer().getRequests().getName()); } + @Bean + @ConditionalOnMissingBean(ServerRequestObservationConvention.class) + @ConditionalOnProperty(name = "management.observations.conventions", havingValue = "opentelemetry") + OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObservationConvention() { + return new OpenTelemetryServerRequestObservationConvention(); + } + } diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfigurationTests.java index b7caed9b2ae..f876b30f0b6 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxObservationAutoConfigurationTests.java @@ -32,6 +32,7 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.reactive.observation.OpenTelemetryServerRequestObservationConvention; import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -107,6 +108,12 @@ class WebFluxObservationAutoConfigurationTests { }); } + @Test + void openTelemetryConventionConfiguredViaProperties() { + this.contextRunner.withPropertyValues("management.observations.conventions=opentelemetry") + .run((context) -> assertThat(context).hasSingleBean(OpenTelemetryServerRequestObservationConvention.class)); + } + @Test void shouldBackOffOnCustomServerRequestObservationConvention() { this.contextRunner 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 492a80d756d..1e9101896ab 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.Observation; 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.context.annotation.Configuration; 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 @@ import org.springframework.web.servlet.DispatcherServlet; 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 0a9989e960a..315def77545 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 @@ -45,6 +45,7 @@ import org.springframework.context.annotation.Bean; 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; @@ -92,6 +93,18 @@ class WebMvcObservationAutoConfigurationTests { }); } + @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)