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
<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:
| metric | type | notes |
|---|---|---|
| resilience.retry.attempts | counter | one per failed attempt |
| resilience.retry.success | counter | one per successful call |
| resilience.circuitbreaker.state | gauge | current phase |
| resilience.circuitbreaker.calls | timer | tag: successful |
| resilience.bulkhead.active_calls | gauge | current permits in use |
| resilience.ratelimiter.remaining_permits | gauge | — |
| resilience.policy.validation_warnings | counter | tags: 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.