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)