Micrometer
Exporta lo que hacen las políticas a cualquier backend de Micrometer (Prometheus, Datadog, OTLP, …): counters de reintentos, rechazos, transiciones del breaker, timeouts y fallbacks, y gauges con el estado de cada circuit breaker, bulkhead y rate limiter.
El módulo resilience4cata-micrometer (disponible desde 0.3.0) está construido sobre los eventos y handles de core; core en sí no depende de Micrometer. Trae io.micrometer:micrometer-core como dependencia transitiva (versión 1.17.1, con la que está probado).
Instalar
Importa el BOM, como para los otros módulos (instalación), y agrega el módulo:
Gradle
dependencies {
implementation platform('dev.resilience4cata:resilience4cata-bom:0.10.0')
implementation 'dev.resilience4cata:resilience4cata-micrometer'
}
Gradle (Kotlin)
dependencies {
implementation(platform("dev.resilience4cata:resilience4cata-bom:0.10.0"))
implementation("dev.resilience4cata:resilience4cata-micrometer")
}
Maven
<!-- with resilience4cata-bom imported in <dependencyManagement> -->
<dependency>
<groupId>dev.resilience4cata</groupId>
<artifactId>resilience4cata-micrometer</artifactId>
</dependency>
Con JPMS, agrega requires dev.resilience4cata.micrometer; (hace requires transitive de dev.resilience4cata.core y de micrometer.core).
Configurar
Tres líneas, alrededor del registry que ya construyes:
MeterRegistry meterRegistry = new SimpleMeterRegistry(); // in your app: the registry you already export
MicrometerMetrics metrics = MicrometerMetrics.create(meterRegistry);
ResilienceRegistry registry = metrics.register(ResilienceRegistry.builder()) // counters, from events
.build();
metrics.bindTo(registry); // gauges, for every breaker, bulkhead and rate limiter, even those created later
register(builder)agrega un listener de eventos al builder y devuelve el builder, para que sigas configurándolo (.retry(…),.listen(…), configuración externa) antes debuild(). Tiene que ejecutarse antes debuild(): los listeners quedan fijos cuando se construye el registry.bindTo(registry)registra los gauges de las instancias que existen y de cada instancia que el registry cree después. Las instancias se crean al envolver un servicio (Resilient.of,Resilient.create) o al construir unPipeline, así que puedes conectar justo después debuild(), antes de envolver nada.
Los servicios anotados y los pipelines se reportan igual. El tag instance es el id de instancia: el name de la anotación, el id derivado pkg.Type#method(java.lang.String) cuando no hay nombre, o el id del pipeline.
Métricas
Los nombres y tags son los de Observabilidad; la columna de Prometheus muestra cómo los exporta un PrometheusMeterRegistry (los puntos pasan a guiones bajos, los counters llevan _total, la unidad percent se vuelve el sufijo _percent).
| Meter | Tipo | Tags | En Prometheus |
|---|---|---|---|
r4c.calls.rejected |
counter | instance, kind = circuitbreaker | ratelimiter | adaptivelimit | bulkhead |
r4c_calls_rejected_total |
r4c.circuitbreaker.transitions |
counter | instance, from, to |
r4c_circuitbreaker_transitions_total |
r4c.retry.retries |
counter | instance |
r4c_retry_retries_total |
r4c.retry.gave.up |
counter | instance, reason |
r4c_retry_gave_up_total |
r4c.hedge.hedged |
counter | instance, reason |
r4c_hedge_hedged_total |
r4c.hedge.won |
counter | instance |
r4c_hedge_won_total |
r4c.hedge.skipped |
counter | instance, reason |
r4c_hedge_skipped_total |
r4c.hedge.failed |
counter | instance |
r4c_hedge_failed_total |
r4c.adaptivelimit.limit.changes |
counter | instance |
r4c_adaptivelimit_limit_changes_total |
r4c.timeout.timed.out |
counter | instance |
r4c_timeout_timed_out_total |
r4c.fallback.applied |
counter | instance |
r4c_fallback_applied_total |
r4c.circuitbreaker.state |
gauge | instance, state |
r4c_circuitbreaker_state |
r4c.circuitbreaker.failure.rate |
gauge, porcentaje | instance |
r4c_circuitbreaker_failure_rate_percent |
r4c.circuitbreaker.slow.call.rate |
gauge, porcentaje | instance |
r4c_circuitbreaker_slow_call_rate_percent |
r4c.bulkhead.concurrent.calls |
gauge | instance |
r4c_bulkhead_concurrent_calls |
r4c.bulkhead.waiting.calls |
gauge | instance |
r4c_bulkhead_waiting_calls |
r4c.bulkhead.max.concurrent |
gauge | instance |
r4c_bulkhead_max_concurrent |
r4c.ratelimiter.available.permits |
gauge | instance |
r4c_ratelimiter_available_permits |
r4c.adaptivelimit.limit |
gauge | instance |
r4c_adaptivelimit_limit |
r4c.adaptivelimit.concurrent.calls |
gauge | instance |
r4c_adaptivelimit_concurrent_calls |
Los valores de los tags van en minúsculas: estados closed, open, half_open, forced_open, disabled; motivos del retry max_attempts, max_duration, not_retryable, interrupted, classifier_failed, budget_exhausted; motivos del hedge delay, failure (hedged) y budget_exhausted, rejected, aborted (skipped) (un motivo agregado en una versión posterior aparece solo).
Cuándo aparece cada meter:
- Los counters se registran en su primer evento: un servicio que nunca reintentó todavía no tiene serie
r4c_retry_retries_total. En PromQL,… or vector(0)llena el hueco cuando necesitas un cero. - Los gauges se registran al conectar la instancia: las cinco series
statedel circuit breaker (una vale 1, las demás 0), sus dos tasas, los tres gauges del bulkhead, el del rate limiter y los dos del adaptive limit. Los retries, hedges, timeouts y fallbacks no tienen gauges. - Las tasas son NaN hasta que la ventana del breaker contiene
minimumCallsllamadas, mientras el breaker está abierto, forzado abierto o desactivado (entonces no hay ventana) y, para la tasa de llamadas lentas, cuando la detección de llamadas lentas está apagada (sinslowCallDuration). Prometheus guarda NaN como un valor; los gráficos muestran un hueco.
Lo que muestra Prometheus
El PaymentGateway de estos ejemplos tiene @Retry(maxAttempts = 3), @CircuitBreaker(windowSize = 4, minimumCalls = 4), un rate limiter de 100 permisos por segundo y un bulkhead de 10, todos con el nombre payments. Después de dos llamadas contra un gateway caído (la primera llamada falla tres veces; el cuarto fallo abre el breaker, que rechaza el reintento de la segunda llamada), un scrape contiene estas líneas. Vienen de un scrape real de PrometheusMeterRegistry en las pruebas del proyecto, sin las líneas # HELP y # TYPE:
r4c_bulkhead_concurrent_calls{instance="payments"} 0.0
r4c_bulkhead_max_concurrent{instance="payments"} 10.0
r4c_bulkhead_waiting_calls{instance="payments"} 0.0
r4c_calls_rejected_total{instance="payments",kind="circuitbreaker"} 1.0
r4c_circuitbreaker_failure_rate_percent{instance="payments"} NaN
r4c_circuitbreaker_slow_call_rate_percent{instance="payments"} NaN
r4c_circuitbreaker_state{instance="payments",state="closed"} 0.0
r4c_circuitbreaker_state{instance="payments",state="disabled"} 0.0
r4c_circuitbreaker_state{instance="payments",state="forced_open"} 0.0
r4c_circuitbreaker_state{instance="payments",state="half_open"} 0.0
r4c_circuitbreaker_state{instance="payments",state="open"} 1.0
r4c_circuitbreaker_transitions_total{from="closed",instance="payments",to="open"} 1.0
r4c_ratelimiter_available_permits{instance="payments"} 98.0
r4c_retry_gave_up_total{instance="payments",reason="max_attempts"} 1.0
r4c_retry_gave_up_total{instance="payments",reason="not_retryable"} 1.0
r4c_retry_retries_total{instance="payments"} 3.0
La tasa de fallos es NaN porque el breaker está abierto. El rate limiter muestra 98 de 100 permisos: cuatro intentos tomaron uno cada uno, y las esperas entre reintentos devolvieron algunos (la prueba corre sobre el reloj manual, así que los números son exactos).
Paneles de Grafana
PromQL para los paneles que necesitan la mayoría de los servicios:
| Panel | Consulta | Notas |
|---|---|---|
| Rechazos por segundo, por política | sum by (instance, kind) (rate(r4c_calls_rejected_total[5m])) |
serie temporal apilada |
| Breakers abiertos ahora | sum by (instance) (r4c_circuitbreaker_state{state=~"open|forced_open"}) |
stat o state timeline; alerta cuando sea > 0 durante unos minutos |
| Transiciones del breaker | increase(r4c_circuitbreaker_transitions_total{to="open"}[1h]) |
un breaker inestable se abre muchas veces |
| Retries por segundo | sum by (instance) (rate(r4c_retry_retries_total[5m])) |
compáralo con tu tasa de requests |
| Abandonos por motivo | sum by (instance, reason) (increase(r4c_retry_gave_up_total[1h])) |
not_retryable incluye las llamadas detenidas por un breaker abierto |
| Tasa de fallos | r4c_circuitbreaker_failure_rate_percent |
huecos hasta minimumCalls, y mientras está abierto |
| Saturación del bulkhead | r4c_bulkhead_concurrent_calls / r4c_bulkhead_max_concurrent |
1 significa lleno; grafica también r4c_bulkhead_waiting_calls |
| Margen del rate limiter | min by (instance) (r4c_ratelimiter_available_permits) |
cerca de 0 significa que se está frenando a los llamadores |
| Timeouts por segundo | sum by (instance) (rate(r4c_timeout_timed_out_total[5m])) |
|
| Fallbacks por segundo | sum by (instance) (rate(r4c_fallback_applied_total[5m])) |
la proporción de respuestas degradadas |
Cardinalidad
Cada serie lleva instance, y sus otros tags tienen un conjunto fijo de valores, así que el número de series crece con el número de instancias, no con el tráfico:
- un circuit breaker: 7 series de gauges, más hasta 25 series de
transitions(en la práctica, las pocas transiciones que ocurren) y una derejected; - un retry: una serie
retriesy una seriegave.uppor cada motivo que ocurrió; - un hedge: hasta dos series
hedged, unawon, unafailedy una serieskippedpor cada motivo que ocurrió; - un bulkhead: 3 gauges y una
rejected; un rate limiter: 1 gauge y unarejected; un timeout o un fallback: un counter.
Mantén acotados los ids de instancia: vienen de tu código (un name de anotación, un método, un id de pipeline), así que lo están, salvo que construyas ids de pipeline con datos de la request. No lo hagas nunca, ni por las métricas ni por las propias políticas, que entonces nunca compartirían estado. Prefiere un name compartido por los métodos que llaman a la misma dependencia: una serie por dependencia se lee mejor que una por método. No agregues tú la clase ni el mensaje de la excepción como tag: en la práctica no están acotados.
Ciclo de vida
- Un
MicrometerMetricsporMeterRegistry. Para exportar a varios backends, pásale unCompositeMeterRegistry. Un mismoMicrometerMetricspuede servir a varios registries de resiliencia: llama aregisteren cada builder y abindToen cada registry. Micrometer identifica un meter solo por nombre y tags, así que si dos registries conectados tienen una instancia con el mismo id, sus counters se suman y se reportan los gauges del registry conectado primero. bindToes idempotente para el mismo registry, y seguro desde cualquier thread, como todo lo demás en la clase.- Nada mantiene vivo un registry descartado. Los gauges sujetan las políticas de forma débil (lo predeterminado en Micrometer), así que el meter registry nunca mantiene alcanzable un
ResilienceRegistry; cuando ese registry es recolectado por el GC, sus gauges reportan NaN. Al revés, un registry conectado mantiene alcanzables elMicrometerMetricsy su meter registry, que es lo que quieres mientras corre. close()quita del meter registry todos los meters que registró, deja de contar eventos y deja de registrar gauges para instancias nuevas. Los servicios siguen funcionando. Llámalo cuando descartes un registry antes del final de la aplicación (en pruebas, o cuando se reinicia un componente):
metrics.close(); // removes every meter it registered; the services keep working
Lo que cuesta
- Una llamada exitosa no cuesta nada más: no publica ningún evento, y los gauges no están en el camino de la llamada.
- Un evento (un reintento, un rechazo, una transición, un timeout, un fallback) cuesta una búsqueda del counter en caché por id de instancia, una lectura de array y el incremento de Micrometer. El primer evento de cada counter lo registra en Micrometer. Aparte del propio record del evento, un evento con el counter en caché no asigna nada (una prueba lo comprueba). Si Micrometer lanza una excepción (un registry que rechaza un meter, por ejemplo),
corela pasa al uncaught exception handler del thread y la llamada sigue. - Un scrape lee los handles; cada una de las dos tasas toma un snapshot del breaker, que recorre su ventana.
- Crear una instancia (al envolver un servicio o construir un pipeline) lista una vez las instancias del registry para registrar los gauges nuevos.
El overhead del módulo todavía no se ha medido.