Micrometer services
View MarkdownStatus: Stable Package:
dev.vertique.micrometer.servicesArtifact:vertique-micrometer-servicesDepends on: io.micrometer:micrometer-core (library), vertique-micrometer-core, vertique-services API reference: javadoc.io
Observe-only service dispatch metrics adapter. When installed alongside DispatchModule and
MicrometerModule, it emits a per-dispatch timer (vertique.service.dispatch) for every terminal
service outcome observed on the consumer side.
The module compiles against vertique-micrometer-core for the MetricsConfig type. It also
requires a MeterRegistry binding on the Dagger graph — normally supplied by MicrometerModule;
without it (or another MeterRegistry provider) the component does not compile. Only the
metrics.enabled gate is optional: the module declares @BindsOptionalOf MetricsConfig
independently, so the gate defaults to enabled when MicrometerModule is absent. It never
modifies the dispatch outcome, and it never submits audit records.
When To Use It
Section titled “When To Use It”Install vertique-micrometer-services whenever an application uses service dispatch via
DispatchModule and metrics are enabled. Pair it with vertique-micrometer-core and at least one
backend module (e.g., vertique-micrometer-registry-prometheus) to make the emitted meters visible.
Without a backend the injected MeterRegistry is an empty composite whose recording is a no-op.
Core Concepts
Section titled “Core Concepts”Consumer-side hook. ServiceDispatchMetricsInterceptor implements ServiceInterceptor and
fires on the consumer side (inside ServiceMethodInvoker). There is no sender-side seam in the
services module; all observations happen after the event-bus message is received and processed.
Terminal outcome, post-recovery. The interceptor overrides ServiceInterceptor#onTerminalComplete
rather than onComplete. onTerminalComplete fires after the recovery pipeline, so recovered
failures appear as SUCCESS — the outcome tag reflects the result the caller ultimately receives.
onComplete fires before recovery and is not used.
Reply timing. The event-bus reply is sent to the caller before onTerminalComplete fires, and
the hook runs only once the afterDispatch handlers and the reply/recovery future have settled (per
the ServiceMethodInvoker implementation). The endTime handed to the interceptor is captured at
that moment, so the recorded duration spans dispatch + handler execution + recovery + afterDispatch
settling. Read it as a server-side measurement that runs past the instant the caller received its
reply, not as caller-observed latency — the timer sample does not exist yet when the reply is
delivered, and nothing this module records reaches the caller.
Per-event registry lookup (no meter cache, D-L). The interceptor calls
Timer.builder(...).tags(...).register(registry) on every event. Micrometer’s internal registry
lookup is the cache. A separate tuple-keyed adapter cache was evaluated and rejected because the
drop-on-saturation variant silently stops recording real time series once legitimate tag combinations
exceed any fixed cap.
Never-throws. Every callback body is wrapped in a try/catch that logs at WARN and swallows. The WARN carries only the exception class name, never the throwable or its message. A misbehaving registry can never affect dispatch processing.
Zero-overhead when unconfigured. Before VertiqueApplication bootstrap the injected
MeterRegistry is an empty composite whose recording is a no-op (NFR-TEL-003). When the optional
MetricsConfig binding is absent (i.e., MicrometerModule is not installed), the interceptor
defaults to enabled.
No module-owned configuration. This module owns no config keys beyond following the
metrics.enabled gate owned by vertique-micrometer-core.
Key Classes
Section titled “Key Classes”MicrometerServicesModule
Section titled “MicrometerServicesModule”Dagger @Module. Contributes one binding:
ServiceDispatchMetricsInterceptorintoSet<ServiceInterceptor>— records the per-dispatch timer on each terminal service outcome.
Also declares @BindsOptionalOf MetricsConfig metricsConfig() so the
interceptor can inject Optional<MetricsConfig> without requiring MicrometerModule
to be installed. When MicrometerModule is also installed its
@Provides MetricsConfig binding satisfies the optional; when absent the optional is
empty and the interceptor defaults to enabled.
@Component(modules = { VertxModule.class, DispatchModule.class, MicrometerModule.class, MicrometerServicesModule.class, // ...})interface AppComponent { /* ... */ }ServiceDispatchMetricsInterceptor
Section titled “ServiceDispatchMetricsInterceptor”@Singleton ServiceInterceptor. Records one timer sample per terminal service outcome via
onTerminalComplete. Meter name: vertique.service.dispatch. When MetricsConfig.enabled() is
false (i.e., metrics.enabled=false in config), returns immediately without recording.
Negative durations (clock skew, test doubles) are clamped to zero before recording.
Invariants and Gotchas
Section titled “Invariants and Gotchas”- Implements only
onTerminalComplete; all otherServiceInterceptorcallbacks use the default no-op implementations. - Recovered failures appear as
outcome=SUCCESSby contract —onTerminalCompletealways sees the final result after recovery. stableTargetId()onServiceDispatchContextis@Nullable; a null value producestarget=UNKNOWN.
Meters
Section titled “Meters”vertique.service.dispatch — Timer
Section titled “vertique.service.dispatch — Timer”Per-dispatch timer. One sample is recorded per terminal ServiceInterceptor#onTerminalComplete
invocation.
| Tag | Values | Notes |
|---|---|---|
target |
Stable dot-delimited target id, e.g. integration.user-service.get-user |
UNKNOWN when ServiceDispatchContext.stableTargetId() is null |
outcome |
SUCCESS or ERROR |
Derived from Result.isSuccess() / Result.isFailure() on the terminal result (post-recovery) |
oneway |
true or false |
true for fire-and-forget (@OneWay) dispatches |
error.type |
Simple class name of the terminal failure cause, e.g. IllegalArgumentException |
none when the outcome is SUCCESS |
All tags above are part of vertique-micrometer-core’s frozen cardinality-guarded tag-key set —
each key is capped at metrics.cardinality.maxTagValuesPerKey distinct values (default 200)
across the composite. See vertique-micrometer-core’s module reference for the guard mechanism.
Outcome convention: a dispatch that failed but was recovered by the service pipeline appears as
outcome=SUCCESS because onTerminalComplete receives the post-recovery result. Only dispatches
whose terminal result is still a failure produce outcome=ERROR.
The adapter’s simple SPI contribution is declared on its injectable implementation with
@RegisterIntoSet. During the provider build, vertique-codegen-dagger emits
GeneratedRegistrationsModule, which this module includes explicitly. The generated module contains
only this type adaptation; configuration, registry, and optional bindings remain hand-written.
Dependencies
Section titled “Dependencies”io.micrometer:micrometer-core—MeterRegistry,Timer,Tags; no Vert.x Micrometer integration types. This is the only third-party Micrometer artifact on the compile classpath.dev.vertique:vertique-micrometer-core—MetricsConfig, themetrics.enabledgate the interceptor injects asOptional<MetricsConfig>.dev.vertique:vertique-services—ServiceInterceptor,ServiceDispatchContext.dev.vertique:vertique-core—Result(fromcore.eventbus), andOrderedExtension, theServiceInterceptorsupertype.io.vertx:vertx-core—Future, the return type of the asyncServiceInterceptorcallbacks this module inherits as no-ops.com.google.dagger:dagger,jakarta.inject:jakarta.inject-apiorg.slf4j:slf4j-api,org.projectlombok:lombok(provided)