From 9edda93e264d33f52346c2096b0f080ccfc41dff Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 28 Oct 2025 11:55:26 +0100 Subject: [PATCH 1/4] fix observation condition --- .../binder/web/WebMetricsServerCondition.java | 15 +++++++++++---- .../observation/FilterCreationSpec.groovy | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsServerCondition.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsServerCondition.java index d99129070..730692a18 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsServerCondition.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsServerCondition.java @@ -25,12 +25,19 @@ public class WebMetricsServerCondition implements Condition { @Override public boolean matches(ConditionContext context) { - boolean isClassPresent = ClassUtils.isPresent("io.micronaut.micrometer.observation.http.server.ObservationServerFilter", context.getBeanContext().getClassLoader()); + boolean observationPresent = ClassUtils.isPresent( + "io.micronaut.micrometer.observation.http.server.ObservationServerFilter", + context.getBeanContext().getClassLoader() + ); - if (!context.containsProperty("micrometer.observation.http.server.enabled") && isClassPresent) { - return false; + if (!observationPresent) { + return true; } - return !context.containsProperty("micrometer.observation.server.server.enabled") || !context.getProperty("micrometer.observation.http.server.enabled", Boolean.class).orElse(false); + boolean observationEnabled = context + .getProperty("micrometer.observation.http.server.enabled", Boolean.class) + .orElse(Boolean.TRUE); + + return !observationEnabled; } } diff --git a/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy b/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy index a0af8aa00..2e13307fe 100644 --- a/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy +++ b/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.micrometer.observation import io.micrometer.core.instrument.MeterRegistry +import io.micrometer.observation.ObservationRegistry import io.micronaut.configuration.metrics.binder.web.ClientMetricsFilter import io.micronaut.configuration.metrics.binder.web.ServerMetricsFilter import io.micronaut.micrometer.observation.http.client.ObservationClientFilter @@ -70,4 +71,21 @@ class FilterCreationSpec extends Specification{ context.getBeansOfType(ServerMetricsFilter).size() == 0 } + void 'check condition both filters enabled'() { + when: + def context = io.micronaut.context.ApplicationContext.builder( + 'micronaut.application.name': 'test-app', + 'micronaut.metrics.binders.web.enabled': 'true', + 'micrometer.observation.http.server.enabled': 'true' + ).start() + + then: + context.getBeansOfType(MeterRegistry).size() == 1 + context.getBeansOfType(ObservationRegistry).size() == 1 + context.getBeansOfType(ObservationClientFilter).size() == 1 + context.getBeansOfType(ObservationServerFilter).size() == 1 + context.getBeansOfType(ClientMetricsFilter).size() == 0 + context.getBeansOfType(ServerMetricsFilter).size() == 0 + } + } From 393fec2b33df85f88b55ecebf21037f661465661 Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 28 Oct 2025 12:21:48 +0100 Subject: [PATCH 2/4] add docs --- src/main/docs/guide/observation.adoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/docs/guide/observation.adoc b/src/main/docs/guide/observation.adoc index fc1768eec..81698b395 100644 --- a/src/main/docs/guide/observation.adoc +++ b/src/main/docs/guide/observation.adoc @@ -29,3 +29,17 @@ You can configure the behavior of this module by modifying the following propert - To disable HTTP server instrumentation, set the `micrometer.observation.http.server.enabled` property to `false` (the default value is `true`). - To disable HTTP client instrumentation, set the `micrometer.observation.http.client.enabled` property to `false` (the default value is `true`). + +==== HTTP server metrics filter selection and fallback + +When the Observation HTTP module is present on the classpath, the HTTP server is instrumented by the Observation-based filter by default, and the Micrometer-based server metrics filter (ServerMetricsFilter) is not created. When the module is absent, the Micrometer-based server metrics filter is created to ensure HTTP metrics are still recorded. + +ServerMetricsFilter instruments Micrometer metrics via MeterRegistry, whereas ObservationServerFilter uses ObservationRegistry and can emit metrics and traces depending on configuration. + +- Observation module present (default): + - `micrometer.observation.http.server.enabled` absent or `true` → ObservationServerFilter active; Micrometer-based ServerMetricsFilter not created. + - `micrometer.observation.http.server.enabled=false` → Observation disabled; Micrometer-based ServerMetricsFilter created (subject to general metrics enablement). +- Observation module absent: + - Any value of `micrometer.observation.http.server.enabled` → Micrometer-based ServerMetricsFilter created. + +This behavior prevents a “no metrics” situation if the observation feature is enabled in configuration but the Observation HTTP module is not on the classpath. The observation property only affects behavior when the Observation HTTP module is available. From 6b589fb12b9019c713452e35aeb9ec33c0f85654 Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Tue, 28 Oct 2025 12:35:15 +0100 Subject: [PATCH 3/4] fix the client as-well --- .../binder/web/WebMetricsClientCondition.java | 15 +++++++++++---- .../observation/FilterCreationSpec.groovy | 19 ++++++++++++++++++- src/main/docs/guide/observation.adoc | 14 ++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsClientCondition.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsClientCondition.java index bfbeabdfe..40028f1c2 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsClientCondition.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsClientCondition.java @@ -25,13 +25,20 @@ public class WebMetricsClientCondition implements Condition { @Override public boolean matches(ConditionContext context) { - boolean isClassPresent = ClassUtils.isPresent("io.micronaut.micrometer.observation.http.client.ObservationClientFilter", context.getBeanContext().getClassLoader()); + boolean observationPresent = ClassUtils.isPresent( + "io.micronaut.micrometer.observation.http.client.ObservationClientFilter", + context.getBeanContext().getClassLoader() + ); - if (!context.containsProperty("micrometer.observation.http.client.enabled") && isClassPresent) { - return false; + if (!observationPresent) { + return true; } - return !context.containsProperty("micrometer.observation.client.server.enabled") || !context.getProperty("micrometer.observation.http.client.enabled", Boolean.class).orElse(false); + boolean observationEnabled = context + .getProperty("micrometer.observation.http.client.enabled", Boolean.class) + .orElse(Boolean.TRUE); + + return !observationEnabled; } } diff --git a/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy b/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy index 2e13307fe..48af27567 100644 --- a/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy +++ b/micrometer-observation-http/src/test/groovy/io/micronaut/micrometer/observation/FilterCreationSpec.groovy @@ -71,7 +71,7 @@ class FilterCreationSpec extends Specification{ context.getBeansOfType(ServerMetricsFilter).size() == 0 } - void 'check condition both filters enabled'() { + void 'check condition both server filters enabled'() { when: def context = io.micronaut.context.ApplicationContext.builder( 'micronaut.application.name': 'test-app', @@ -88,4 +88,21 @@ class FilterCreationSpec extends Specification{ context.getBeansOfType(ServerMetricsFilter).size() == 0 } + void 'check condition both client filters enabled'() { + when: + def context = io.micronaut.context.ApplicationContext.builder( + 'micronaut.application.name': 'test-app', + 'micronaut.metrics.binders.web.enabled': 'true', + 'micrometer.observation.http.client.enabled': 'true' + ).start() + + then: + context.getBeansOfType(MeterRegistry).size() == 1 + context.getBeansOfType(ObservationRegistry).size() == 1 + context.getBeansOfType(ObservationClientFilter).size() == 1 + context.getBeansOfType(ObservationServerFilter).size() == 1 + context.getBeansOfType(ClientMetricsFilter).size() == 0 + context.getBeansOfType(ServerMetricsFilter).size() == 0 + } + } diff --git a/src/main/docs/guide/observation.adoc b/src/main/docs/guide/observation.adoc index 81698b395..c7694c344 100644 --- a/src/main/docs/guide/observation.adoc +++ b/src/main/docs/guide/observation.adoc @@ -43,3 +43,17 @@ ServerMetricsFilter instruments Micrometer metrics via MeterRegistry, whereas Ob - Any value of `micrometer.observation.http.server.enabled` → Micrometer-based ServerMetricsFilter created. This behavior prevents a “no metrics” situation if the observation feature is enabled in configuration but the Observation HTTP module is not on the classpath. The observation property only affects behavior when the Observation HTTP module is available. + +==== HTTP client metrics filter selection and fallback + +When the Observation HTTP module is present on the classpath, the HTTP client is instrumented by the Observation-based filter by default, and the Micrometer-based client metrics filter (ClientMetricsFilter) is not created. When the module is absent, the Micrometer-based client metrics filter is created to ensure HTTP client metrics are still recorded. + +ClientMetricsFilter instruments Micrometer metrics via MeterRegistry, whereas ObservationClientFilter uses ObservationRegistry and can emit metrics and traces depending on configuration. + +- Observation module present (default): + - `micrometer.observation.http.client.enabled` absent or `true` → ObservationClientFilter active; Micrometer-based ClientMetricsFilter not created. + - `micrometer.observation.http.client.enabled=false` → Observation disabled; Micrometer-based ClientMetricsFilter created (subject to general metrics enablement). +- Observation module absent: + - Any value of `micrometer.observation.http.client.enabled` → Micrometer-based ClientMetricsFilter created. + +This mirrors the server-side selection and avoids a “no metrics” situation when the Observation HTTP module is missing. From add3ee0fe8a8889089146a9cc2e2d041f84447ec Mon Sep 17 00:00:00 2001 From: Nemanja Mikic Date: Wed, 29 Oct 2025 16:20:20 +0100 Subject: [PATCH 4/4] register ObservationRegistry globally --- micrometer-observation/build.gradle | 2 ++ .../observation/DefaultObservedFactory.java | 9 ++++++ .../DefaultObservedFactorySpec.groovy | 32 ++++++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/micrometer-observation/build.gradle b/micrometer-observation/build.gradle index b47e88074..2bba908fe 100644 --- a/micrometer-observation/build.gradle +++ b/micrometer-observation/build.gradle @@ -10,6 +10,8 @@ dependencies { implementation mn.reactor implementation mn.micronaut.core.reactive + implementation mn.micronaut.context + compileOnly(libs.managed.micrometer.tracing) testAnnotationProcessor mn.micronaut.inject.java diff --git a/micrometer-observation/src/main/java/io/micronaut/micrometer/observation/DefaultObservedFactory.java b/micrometer-observation/src/main/java/io/micronaut/micrometer/observation/DefaultObservedFactory.java index f62d5b8a7..f151ed7f1 100644 --- a/micrometer-observation/src/main/java/io/micronaut/micrometer/observation/DefaultObservedFactory.java +++ b/micrometer-observation/src/main/java/io/micronaut/micrometer/observation/DefaultObservedFactory.java @@ -24,6 +24,9 @@ import io.micrometer.observation.ObservationHandler; import io.micrometer.observation.ObservationPredicate; import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.Observations; +import io.micronaut.runtime.event.annotation.EventListener; +import io.micronaut.context.event.ShutdownEvent; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; import io.micrometer.tracing.handler.TracingObservationHandler; @@ -85,6 +88,7 @@ ObservationRegistry observationRegistry( observationPredicates.forEach(observationRegistry.observationConfig()::observationPredicate); observationFilters.forEach(observationRegistry.observationConfig()::observationFilter); observationConventions.forEach(observationRegistry.observationConfig()::observationConvention); + Observations.setRegistry(observationRegistry); return observationRegistry; } @@ -159,4 +163,9 @@ ObservationHandlerGroupingClass observationHandlerGroupingClassMeterTracer() { return new ObservationHandlerGroupingClass(TracingObservationHandler.class); } + @EventListener + void onShutdown(ShutdownEvent shutdown) { + Observations.resetRegistry(); + } + } diff --git a/micrometer-observation/src/test/groovy/io/micronaut/micrometer/observation/DefaultObservedFactorySpec.groovy b/micrometer-observation/src/test/groovy/io/micronaut/micrometer/observation/DefaultObservedFactorySpec.groovy index 96f80205d..b2ff0c314 100644 --- a/micrometer-observation/src/test/groovy/io/micronaut/micrometer/observation/DefaultObservedFactorySpec.groovy +++ b/micrometer-observation/src/test/groovy/io/micronaut/micrometer/observation/DefaultObservedFactorySpec.groovy @@ -4,11 +4,13 @@ import io.micrometer.core.instrument.MeterRegistry import io.micrometer.observation.ObservationFilter import io.micrometer.observation.ObservationHandler import io.micrometer.observation.ObservationRegistry +import io.micrometer.observation.Observations import io.micrometer.tracing.Tracer import io.micrometer.tracing.handler.DefaultTracingObservationHandler import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler import io.micrometer.tracing.propagation.Propagator +import io.micronaut.context.ApplicationContext import spock.lang.Specification class DefaultObservedFactorySpec extends Specification { @@ -20,7 +22,7 @@ class DefaultObservedFactorySpec extends Specification { void 'test no metrics and no trace'() { when: - def context = io.micronaut.context.ApplicationContext.builder( + def context = ApplicationContext.builder( 'micronaut.application.name': 'test-app', ).start() @@ -35,9 +37,31 @@ class DefaultObservedFactorySpec extends Specification { context.getBeansOfType(ObservationRegistry).size() == 1 } + void 'observations global registry is set on startup and reset on shutdown'() { + given: + Observations.resetRegistry() + def initialConfig = Observations.getGlobalRegistry().observationConfig() + + when: + def context = ApplicationContext.builder( + 'micronaut.application.name': 'test-app', + ).start() + + then: + def registryBean = context.getBean(ObservationRegistry) + Observations.getGlobalRegistry().observationConfig().is(registryBean.observationConfig()) + + when: + context.close() + + then: + Observations.getGlobalRegistry().observationConfig().is(initialConfig) + } + + void 'test metrics and no trace'() { when: - def context = io.micronaut.context.ApplicationContext.builder( + def context = ApplicationContext.builder( 'micronaut.application.name': 'test-app', ).start() context.registerSingleton(meterRegistryMocked) @@ -56,7 +80,7 @@ class DefaultObservedFactorySpec extends Specification { void 'test trace and no metrics'() { when: - def context = io.micronaut.context.ApplicationContext.builder( + def context = ApplicationContext.builder( 'micronaut.application.name': 'test-app', ).start() context.registerSingleton(tracerMocked) @@ -95,7 +119,7 @@ class DefaultObservedFactorySpec extends Specification { void 'test metrics and trace with propagator'() { when: - def context = io.micronaut.context.ApplicationContext.builder( + def context = ApplicationContext.builder( 'micronaut.application.name': 'test-app', ).start() context.registerSingleton(meterRegistryMocked)