Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -70,4 +71,38 @@ class FilterCreationSpec extends Specification{
context.getBeansOfType(ServerMetricsFilter).size() == 0
}

void 'check condition both server 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
}

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
}

}
2 changes: 2 additions & 0 deletions micrometer-observation/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -159,4 +163,9 @@ ObservationHandlerGroupingClass observationHandlerGroupingClassMeterTracer() {
return new ObservationHandlerGroupingClass(TracingObservationHandler.class);
}

@EventListener
void onShutdown(ShutdownEvent shutdown) {
Observations.resetRegistry();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()

Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions src/main/docs/guide/observation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,31 @@ 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.

==== 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.
Loading