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
This commit is contained in:
Tommy Ludwig
2026-09-17 09:41:16 +01:00
committed by Andy Wilkinson
parent 254a2e37ba
commit 7d43f31732
9 changed files with 228 additions and 26 deletions
@@ -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.JvmMemoryCommittedMeterConvention;
import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMaxMeterConvention; 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.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.JvmMemoryUsedMeterConvention;
import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadCountMeterConvention; 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.JvmThreadMeterConventions;
@@ -49,9 +50,12 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.context.properties.PropertyMapper;
import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.annotation.ImportRuntimeHints;
@@ -67,6 +71,7 @@ import org.springframework.util.ClassUtils;
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@ConditionalOnClass(MeterRegistry.class) @ConditionalOnClass(MeterRegistry.class)
@ConditionalOnBean(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class)
@EnableConfigurationProperties(ObservationProperties.class)
public final class JvmMetricsAutoConfiguration { public final class JvmMetricsAutoConfiguration {
private static final String VIRTUAL_THREAD_METRICS_CLASS = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics"; private static final String VIRTUAL_THREAD_METRICS_CLASS = "io.micrometer.java21.instrument.binder.jdk.VirtualThreadMetrics";
@@ -86,26 +91,29 @@ public final class JvmMetricsAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
JvmMemoryMetrics jvmMemoryMetrics(ObjectProvider<JvmMemoryMeterConventions> jvmMemoryMeterConventions, JvmMemoryMetrics jvmMemoryMetrics(ObservationProperties observationProperties,
ObjectProvider<JvmMemoryMeterConventions> jvmMemoryMeterConventions,
ObjectProvider<JvmMemoryUsedMeterConvention> jvmMemoryUsedMeterConvention, ObjectProvider<JvmMemoryUsedMeterConvention> jvmMemoryUsedMeterConvention,
ObjectProvider<JvmMemoryCommittedMeterConvention> jvmMemoryCommittedMeterConvention, ObjectProvider<JvmMemoryCommittedMeterConvention> jvmMemoryCommittedMeterConvention,
ObjectProvider<JvmMemoryMaxMeterConvention> jvmMemoryMaxMeterConvention) { ObjectProvider<JvmMemoryMaxMeterConvention> jvmMemoryMaxMeterConvention,
ObjectProvider<JvmMemoryUsedAfterLastGcMeterConvention> jvmMemoryUsedAfterLastGcMeterConvention) {
JvmMemoryMetricsFactory factory = new JvmMemoryMetricsFactory(jvmMemoryUsedMeterConvention.getIfAvailable(), JvmMemoryMetricsFactory factory = new JvmMemoryMetricsFactory(jvmMemoryUsedMeterConvention.getIfAvailable(),
jvmMemoryCommittedMeterConvention.getIfAvailable(), jvmMemoryMaxMeterConvention.getIfAvailable()); jvmMemoryCommittedMeterConvention.getIfAvailable(), jvmMemoryMaxMeterConvention.getIfAvailable(),
jvmMemoryUsedAfterLastGcMeterConvention.getIfAvailable());
JvmMemoryMeterConventions deprecatedConventions = jvmMemoryMeterConventions.getIfAvailable(); JvmMemoryMeterConventions deprecatedConventions = jvmMemoryMeterConventions.getIfAvailable();
if (deprecatedConventions != null && factory.hasConvention()) { if (deprecatedConventions != null && factory.hasConvention()) {
throw new IllegalStateException("Either %s or the interfaces that supersede it should be set" throw new IllegalStateException("Either %s or the interfaces that supersede it should be set"
.formatted(JvmMemoryMeterConventions.class.getSimpleName())); .formatted(JvmMemoryMeterConventions.class.getSimpleName()));
} }
return (deprecatedConventions != null) ? new JvmMemoryMetrics(Collections.emptyList(), deprecatedConventions) return (deprecatedConventions != null) ? new JvmMemoryMetrics(Collections.emptyList(), deprecatedConventions)
: factory.create(); : factory.create(observationProperties);
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
JvmThreadMetrics jvmThreadMetrics(ObjectProvider<JvmThreadMeterConventions> jvmThreadMeterConventions, JvmThreadMetrics jvmThreadMetrics(ObservationProperties observationProperties,
ObjectProvider<JvmThreadMeterConventions> jvmThreadMeterConventions,
ObjectProvider<JvmThreadCountMeterConvention> jvmThreadCountMeterConvention) { ObjectProvider<JvmThreadCountMeterConvention> jvmThreadCountMeterConvention) {
JvmThreadMeterConventions deprecatedConventions = jvmThreadMeterConventions.getIfAvailable(); JvmThreadMeterConventions deprecatedConventions = jvmThreadMeterConventions.getIfAvailable();
JvmThreadCountMeterConvention convention = jvmThreadCountMeterConvention.getIfAvailable(); JvmThreadCountMeterConvention convention = jvmThreadCountMeterConvention.getIfAvailable();
@@ -117,14 +125,19 @@ public final class JvmMetricsAutoConfiguration {
if (deprecatedConventions != null) { if (deprecatedConventions != null) {
return new JvmThreadMetrics(Collections.emptyList(), deprecatedConventions); return new JvmThreadMetrics(Collections.emptyList(), deprecatedConventions);
} }
return (convention != null) ? JvmThreadMetrics.builder().threadCountConvention(convention).build() JvmThreadMetrics.Builder builder = JvmThreadMetrics.builder();
: new JvmThreadMetrics(); if (observationProperties.getConventions() == ConventionsVariant.OPENTELEMETRY) {
builder.openTelemetryConventions();
}
PropertyMapper map = PropertyMapper.get();
map.from(convention).to(builder::threadCountConvention);
return builder.build();
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
ClassLoaderMetrics classLoaderMetrics( ClassLoaderMetrics classLoaderMetrics(ObservationProperties observationProperties,
ObjectProvider<JvmClassLoadingMeterConventions> jvmClassLoadingMeterConventions, ObjectProvider<JvmClassLoadingMeterConventions> jvmClassLoadingMeterConventions,
ObjectProvider<JvmClassCountMeterConvention> jvmClassCountMeterConvention, ObjectProvider<JvmClassCountMeterConvention> jvmClassCountMeterConvention,
ObjectProvider<JvmClassLoadedMeterConvention> jvmClassLoadedMeterConvention, ObjectProvider<JvmClassLoadedMeterConvention> jvmClassLoadedMeterConvention,
@@ -136,7 +149,8 @@ public final class JvmMetricsAutoConfiguration {
throw new IllegalStateException("Either %s or the interfaces that supersede it should be set" throw new IllegalStateException("Either %s or the interfaces that supersede it should be set"
.formatted(JvmClassLoadingMeterConventions.class.getSimpleName())); .formatted(JvmClassLoadingMeterConventions.class.getSimpleName()));
} }
return (deprecatedConventions != null) ? new ClassLoaderMetrics(deprecatedConventions) : factory.create(); return (deprecatedConventions != null) ? new ClassLoaderMetrics(deprecatedConventions)
: factory.create(observationProperties);
} }
@Bean @Bean
@@ -168,19 +182,24 @@ public final class JvmMetricsAutoConfiguration {
private record JvmMemoryMetricsFactory(@Nullable JvmMemoryUsedMeterConvention memoryUsedConvention, private record JvmMemoryMetricsFactory(@Nullable JvmMemoryUsedMeterConvention memoryUsedConvention,
@Nullable JvmMemoryCommittedMeterConvention memoryCommittedConvention, @Nullable JvmMemoryCommittedMeterConvention memoryCommittedConvention,
@Nullable JvmMemoryMaxMeterConvention memoryMaxConvention) { @Nullable JvmMemoryMaxMeterConvention memoryMaxConvention,
@Nullable JvmMemoryUsedAfterLastGcMeterConvention memoryUsedAfterLastGcConvention) {
boolean hasConvention() { boolean hasConvention() {
return this.memoryUsedConvention != null || this.memoryCommittedConvention != null 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(); JvmMemoryMetrics.Builder builder = JvmMemoryMetrics.builder();
if (observationProperties.getConventions() == ConventionsVariant.OPENTELEMETRY) {
builder.openTelemetryConventions();
}
PropertyMapper map = PropertyMapper.get(); PropertyMapper map = PropertyMapper.get();
map.from(this.memoryUsedConvention).to(builder::memoryUsedConvention); map.from(this.memoryUsedConvention).to(builder::memoryUsedConvention);
map.from(this.memoryCommittedConvention).to(builder::memoryCommittedConvention); map.from(this.memoryCommittedConvention).to(builder::memoryCommittedConvention);
map.from(this.memoryMaxConvention).to(builder::memoryMaxConvention); map.from(this.memoryMaxConvention).to(builder::memoryMaxConvention);
map.from(this.memoryUsedAfterLastGcConvention).to(builder::memoryUsedAfterLastGcConvention);
return builder.build(); return builder.build();
} }
@@ -195,8 +214,11 @@ public final class JvmMetricsAutoConfiguration {
|| this.classUnloadedConvention != null; || this.classUnloadedConvention != null;
} }
ClassLoaderMetrics create() { ClassLoaderMetrics create(ObservationProperties observationProperties) {
ClassLoaderMetrics.Builder builder = ClassLoaderMetrics.builder(); ClassLoaderMetrics.Builder builder = ClassLoaderMetrics.builder();
if (observationProperties.getConventions() == ConventionsVariant.OPENTELEMETRY) {
builder.openTelemetryConventions();
}
PropertyMapper map = PropertyMapper.get(); PropertyMapper map = PropertyMapper.get();
map.from(this.classCountConvention).to(builder::classCountConvention); map.from(this.classCountConvention).to(builder::classCountConvention);
map.from(this.classLoadedConvention).to(builder::classLoadedConvention); map.from(this.classLoadedConvention).to(builder::classLoadedConvention);
@@ -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.MetricsAutoConfiguration;
import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsProperties; import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsProperties;
import org.springframework.boot.micrometer.metrics.system.DiskSpaceMetricsBinder; 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; import org.springframework.context.annotation.Bean;
/** /**
@@ -55,7 +57,7 @@ import org.springframework.context.annotation.Bean;
@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class })
@ConditionalOnClass(MeterRegistry.class) @ConditionalOnClass(MeterRegistry.class)
@ConditionalOnBean(MeterRegistry.class) @ConditionalOnBean(MeterRegistry.class)
@EnableConfigurationProperties(MetricsProperties.class) @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
public final class SystemMetricsAutoConfiguration { public final class SystemMetricsAutoConfiguration {
@Bean @Bean
@@ -67,7 +69,8 @@ public final class SystemMetricsAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
ProcessorMetrics processorMetrics(ObjectProvider<JvmCpuMeterConventions> jvmCpuMeterConventions, ProcessorMetrics processorMetrics(ObservationProperties observationProperties,
ObjectProvider<JvmCpuMeterConventions> jvmCpuMeterConventions,
ObjectProvider<JvmCpuTimeMeterConvention> jvmCpuTimeMeterConvention, ObjectProvider<JvmCpuTimeMeterConvention> jvmCpuTimeMeterConvention,
ObjectProvider<JvmCpuCountMeterConvention> jvmCpuCountMeterConvention, ObjectProvider<JvmCpuCountMeterConvention> jvmCpuCountMeterConvention,
ObjectProvider<JvmCpuLoadMeterConvention> jvmCpuLoadMeterConvention) { ObjectProvider<JvmCpuLoadMeterConvention> jvmCpuLoadMeterConvention) {
@@ -79,7 +82,7 @@ public final class SystemMetricsAutoConfiguration {
.formatted(JvmCpuMeterConventions.class.getSimpleName())); .formatted(JvmCpuMeterConventions.class.getSimpleName()));
} }
return (deprecatedConventions != null) ? new ProcessorMetrics(Collections.emptyList(), deprecatedConventions) return (deprecatedConventions != null) ? new ProcessorMetrics(Collections.emptyList(), deprecatedConventions)
: factory.create(); : factory.create(observationProperties);
} }
@Bean @Bean
@@ -103,9 +106,12 @@ public final class SystemMetricsAutoConfiguration {
return this.cpuTimeConvention != null || this.cpuCountConvention != null || this.cpuLoadConvention != null; return this.cpuTimeConvention != null || this.cpuCountConvention != null || this.cpuLoadConvention != null;
} }
ProcessorMetrics create() { ProcessorMetrics create(ObservationProperties observationProperties) {
PropertyMapper map = PropertyMapper.get();
ProcessorMetrics.Builder builder = ProcessorMetrics.builder(); 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.cpuTimeConvention).to(builder::cpuTimeConvention);
map.from(this.cpuCountConvention).to(builder::cpuCountConvention); map.from(this.cpuCountConvention).to(builder::cpuCountConvention);
map.from(this.cpuLoadConvention).to(builder::cpuLoadConvention); map.from(this.cpuLoadConvention).to(builder::cpuLoadConvention);
@@ -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.JvmMemoryCommittedMeterConvention;
import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMaxMeterConvention; 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.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.JvmMemoryUsedMeterConvention;
import io.micrometer.core.instrument.binder.jvm.convention.JvmThreadCountMeterConvention; 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.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.MicrometerJvmClassLoadingMeterConventions;
import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmMemoryMeterConventions; 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.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 io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.EnabledForJreRange;
@@ -80,6 +90,62 @@ class JvmMetricsAutoConfigurationTests {
this.contextRunner.run(assertMetricsBeans()); 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 @Test
void allowsCustomJvmGcMetricsToBeUsed() { void allowsCustomJvmGcMetricsToBeUsed() {
this.contextRunner.withUserConfiguration(CustomJvmGcMetricsConfiguration.class) this.contextRunner.withUserConfiguration(CustomJvmGcMetricsConfiguration.class)
@@ -171,6 +237,17 @@ class JvmMetricsAutoConfigurationTests {
.hasFieldOrPropertyWithValue("memoryMaxConvention", memoryMaxConvention)); .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 @Test
void allowsCustomJvmThreadMetricsToBeUsed() { void allowsCustomJvmThreadMetricsToBeUsed() {
this.contextRunner.withUserConfiguration(CustomJvmThreadMetricsConfiguration.class) this.contextRunner.withUserConfiguration(CustomJvmThreadMetricsConfiguration.class)
@@ -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.JvmCpuLoadMeterConvention;
import io.micrometer.core.instrument.binder.jvm.convention.JvmCpuMeterConventions; 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.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.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.FileDescriptorMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics; import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.core.instrument.binder.system.UptimeMetrics; import io.micrometer.core.instrument.binder.system.UptimeMetrics;
@@ -73,6 +76,35 @@ class SystemMetricsAutoConfigurationTests {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ProcessorMetrics.class)); 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 @Test
void allowsCustomProcessorMetricsToBeUsed() { void allowsCustomProcessorMetricsToBeUsed() {
this.contextRunner.withUserConfiguration(CustomProcessorMetricsConfiguration.class) this.contextRunner.withUserConfiguration(CustomProcessorMetricsConfiguration.class)
@@ -45,6 +45,11 @@ public class ObservationProperties {
*/ */
private Map<String, Boolean> enable = new LinkedHashMap<>(); private Map<String, Boolean> enable = new LinkedHashMap<>();
/**
* Conventions variant to use when configuring observations and metrics.
*/
private ConventionsVariant conventions = ConventionsVariant.MICROMETER;
public Map<String, Boolean> getEnable() { public Map<String, Boolean> getEnable() {
return this.enable; return this.enable;
} }
@@ -65,6 +70,20 @@ public class ObservationProperties {
this.keyValues = 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 { public static class Http {
private final Client client = new Client(); private final Client client = new Client();
@@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties; 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.context.annotation.Bean;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; 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.http.server.reactive.observation.ServerRequestObservationConvention;
/** /**
@@ -71,9 +73,18 @@ public final class WebFluxObservationAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(ServerRequestObservationConvention.class) @ConditionalOnMissingBean(ServerRequestObservationConvention.class)
DefaultServerRequestObservationConvention defaultServerRequestObservationConvention() { @ConditionalOnProperty(name = "management.observations.conventions", havingValue = "micrometer",
matchIfMissing = true)
DefaultServerRequestObservationConvention micrometerServerRequestObservationConvention() {
return new DefaultServerRequestObservationConvention( return new DefaultServerRequestObservationConvention(
this.observationProperties.getHttp().getServer().getRequests().getName()); this.observationProperties.getHttp().getServer().getRequests().getName());
} }
@Bean
@ConditionalOnMissingBean(ServerRequestObservationConvention.class)
@ConditionalOnProperty(name = "management.observations.conventions", havingValue = "opentelemetry")
OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObservationConvention() {
return new OpenTelemetryServerRequestObservationConvention();
}
} }
@@ -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.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; 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.http.server.reactive.observation.ServerRequestObservationConvention;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; 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 @Test
void shouldBackOffOnCustomServerRequestObservationConvention() { void shouldBackOffOnCustomServerRequestObservationConvention() {
this.contextRunner this.contextRunner
@@ -21,12 +21,13 @@ import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.DispatcherType; import jakarta.servlet.DispatcherType;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 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.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties; 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.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.OpenTelemetryServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationConvention; import org.springframework.http.server.observation.ServerRequestObservationConvention;
import org.springframework.web.filter.ServerHttpObservationFilter; import org.springframework.web.filter.ServerHttpObservationFilter;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
@@ -64,13 +66,26 @@ import org.springframework.web.servlet.DispatcherServlet;
public final class WebMvcObservationAutoConfiguration { public final class WebMvcObservationAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingFilterBean @ConditionalOnMissingBean(ServerRequestObservationConvention.class)
FilterRegistrationBean<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry, @ConditionalOnProperty(name = "management.observations.conventions", havingValue = "micrometer",
ObjectProvider<ServerRequestObservationConvention> customConvention, matchIfMissing = true)
DefaultServerRequestObservationConvention micrometerServerRequestObservationConvention(
ObservationProperties observationProperties) { ObservationProperties observationProperties) {
String name = observationProperties.getHttp().getServer().getRequests().getName(); String name = observationProperties.getHttp().getServer().getRequests().getName();
ServerRequestObservationConvention convention = customConvention return new DefaultServerRequestObservationConvention(name);
.getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); }
@Bean
@ConditionalOnMissingBean(ServerRequestObservationConvention.class)
@ConditionalOnProperty(name = "management.observations.conventions", havingValue = "opentelemetry")
OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObservationConvention() {
return new OpenTelemetryServerRequestObservationConvention();
}
@Bean
@ConditionalOnMissingFilterBean
FilterRegistrationBean<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
ServerRequestObservationConvention convention) {
ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention);
FilterRegistrationBean<ServerHttpObservationFilter> registration = new FilterRegistrationBean<>(filter); FilterRegistrationBean<ServerHttpObservationFilter> registration = new FilterRegistrationBean<>(filter);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
@@ -45,6 +45,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; 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.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; 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 @Test
void customConventionWhenPresent() { void customConventionWhenPresent() {
this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class) this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class)