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.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> jvmMemoryMeterConventions,
JvmMemoryMetrics jvmMemoryMetrics(ObservationProperties observationProperties,
ObjectProvider<JvmMemoryMeterConventions> jvmMemoryMeterConventions,
ObjectProvider<JvmMemoryUsedMeterConvention> jvmMemoryUsedMeterConvention,
ObjectProvider<JvmMemoryCommittedMeterConvention> jvmMemoryCommittedMeterConvention,
ObjectProvider<JvmMemoryMaxMeterConvention> jvmMemoryMaxMeterConvention) {
ObjectProvider<JvmMemoryMaxMeterConvention> jvmMemoryMaxMeterConvention,
ObjectProvider<JvmMemoryUsedAfterLastGcMeterConvention> 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> jvmThreadMeterConventions,
JvmThreadMetrics jvmThreadMetrics(ObservationProperties observationProperties,
ObjectProvider<JvmThreadMeterConventions> jvmThreadMeterConventions,
ObjectProvider<JvmThreadCountMeterConvention> 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> jvmClassLoadingMeterConventions,
ObjectProvider<JvmClassCountMeterConvention> jvmClassCountMeterConvention,
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"
.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);
@@ -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> jvmCpuMeterConventions,
ProcessorMetrics processorMetrics(ObservationProperties observationProperties,
ObjectProvider<JvmCpuMeterConventions> jvmCpuMeterConventions,
ObjectProvider<JvmCpuTimeMeterConvention> jvmCpuTimeMeterConvention,
ObjectProvider<JvmCpuCountMeterConvention> jvmCpuCountMeterConvention,
ObjectProvider<JvmCpuLoadMeterConvention> 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);
@@ -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)
@@ -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)
@@ -45,6 +45,11 @@ public class ObservationProperties {
*/
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() {
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();
@@ -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();
}
}
@@ -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
@@ -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<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
ObjectProvider<ServerRequestObservationConvention> 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<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
ServerRequestObservationConvention convention) {
ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention);
FilterRegistrationBean<ServerHttpObservationFilter> registration = new FilterRegistrationBean<>(filter);
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.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)