Skip to main content

Metrics with Micrometer

Wire pattern and Policy events into Micrometer counters, gauges and timers with resiliencia-metrics and resiliencia-micrometer.

Patterns already emit typed events for every call outcome and state transition — metrics are just another listener on that same event stream. resiliencia-metrics defines a backend-neutral contract; resiliencia-micrometer implements it against a Micrometer MeterRegistry. Neither is required — by default, nothing is recorded.

Add the dependencies

pom.xml
<dependency>
<groupId>io.github.teceli</groupId>
<artifactId>resiliencia-metrics</artifactId>
<version>1.0.0-beta.1</version>
</dependency>
<dependency>
<groupId>io.github.teceli</groupId>
<artifactId>resiliencia-micrometer</artifactId>
<version>1.0.0-beta.1</version>
</dependency>

resiliencia-metrics has no dependency on Micrometer itself — it's the neutral contract. resiliencia-micrometer is the module that actually depends on micrometer-core.

The ResilienceMetrics contract

Two methods, not one per pattern:

public interface ResilienceMetrics {
void observe(Snapshot snapshot);
void observe(Counters counters);
}

Snapshot (gauge-like, current-state values — circuit state, active bulkhead calls, remaining rate-limiter permits) and Counters (event-like — a retry attempt failed, a call was rejected) are both sealed hierarchies, one variant per thing a pattern can report. When no backend is configured, patterns use NoOpMetrics.INSTANCE, which discards everything — you don't need to null-check or wire up a backend you don't want.

Wire it up

MicrometerResilienceMetrics adapts the contract onto a MeterRegistry. ResilienceMetricsListener translates pattern and Policy events into calls against it, and attaches like any other listener via withListener(...):

import io.github.teceli.resiliencia.metrics.ResilienceMetricsListener;
import io.github.teceli.resiliencia.micrometer.MicrometerResilienceMetrics;

var metrics = new MicrometerResilienceMetrics(meterRegistry);
var listener = new ResilienceMetricsListener(metrics);

var circuitBreaker = CircuitBreaker.of("orders-api").withListener(listener);
var policy = Policy.compose(circuitBreaker).and(retry).withListener(listener);

Attach the same listener to every pattern and to the Policy wrapping them — Policy only emits its own validation-warning events, so the individual patterns still need the listener too in order to report their own metrics.

Every metric is tagged with the pattern's name, so name each instance something you'd recognize on a dashboard — and keep names static and reused across calls (patterns are constructed once, not per-request), since an unbounded set of names becomes an unbounded set of time series in Micrometer.

Naming convention

Metrics are dot-namespaced as resilience.<pattern>.<metric> — the separator both Micrometer and OTel already use natively. A few examples:

metrictypenotes
resilience.retry.attemptscounterone per failed attempt
resilience.retry.successcounterone per successful call
resilience.circuitbreaker.stategaugecurrent phase
resilience.circuitbreaker.callstimertag: successful
resilience.bulkhead.active_callsgaugecurrent permits in use
resilience.ratelimiter.remaining_permitsgauge
resilience.policy.validation_warningscountertags: outer, inner

A broken backend never breaks the call it's observing — a failure inside observe(...) is caught, logged at WARN, and the protected call proceeds unaffected.