Skip to content

Micrometer services

View Markdown

Status: Stable Package: dev.vertique.micrometer.services Artifact: vertique-micrometer-services Depends 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.


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.


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.


Dagger @Module. Contributes one binding:

  • ServiceDispatchMetricsInterceptor into Set<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 { /* ... */ }

@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.

  • Implements only onTerminalComplete; all other ServiceInterceptor callbacks use the default no-op implementations.
  • Recovered failures appear as outcome=SUCCESS by contract — onTerminalComplete always sees the final result after recovery.
  • stableTargetId() on ServiceDispatchContext is @Nullable; a null value produces target=UNKNOWN.

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.

  • 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, the metrics.enabled gate the interceptor injects as Optional<MetricsConfig>.
  • dev.vertique:vertique-services — ServiceInterceptor, ServiceDispatchContext.
  • dev.vertique:vertique-core — Result (from core.eventbus), and OrderedExtension, the ServiceInterceptor supertype.
  • io.vertx:vertx-core — Future, the return type of the async ServiceInterceptor callbacks this module inherits as no-ops.
  • com.google.dagger:dagger, jakarta.inject:jakarta.inject-api
  • org.slf4j:slf4j-api, org.projectlombok:lombok (provided)