Decisiones de monitoreo

El arquetipo configura una superficie de monitoreo conservadora: pocos endpoints, detalles solo para usuarios autorizados, y tres capas separadas para observar las bandejas de mensajeria. Cada una de esas elecciones tiene una alternativa razonable que se descarto, y entender el por que es lo que permite saber cuando ajustarlas en un servicio especifico. Para el catalogo concreto (endpoints, propiedades, contratos JSON) ver Monitoreo.

Por que allowlist y no allowlist negativa

Spring Boot Actuator expone por defecto un set minimo (health, info), pero con management.endpoints.web.exposure.include: '' o un exclude parcial, todos los endpoints auto-configurados pasan a responder. La diferencia es de *fail-safe:

  • Allowlist (include: health, info, …​): un endpoint nuevo que se agregue en una version futura de Spring Boot — o una dependencia que registre su propio endpoint — no se expone hasta que alguien lo agregue explicitamente. El default es "no expuesto".

  • Allowlist negativa (include: '*', exclude: env, beans, …​): un endpoint nuevo se expone automaticamente. El default es "expuesto".

El arquetipo asume que el costo de descubrir manualmente que se necesita un endpoint extra es menor que el costo de exponer accidentalmente algo sensible. Por eso la lista de exclusiones en Endpoints excluidos no tiene efecto operativo — esta documentada solo para explicar que se decidio no exponer.

Por que show-details: when-authorized

always filtra informacion util para un atacante no autenticado: connection strings cuando el indicador de datasource esta DOWN, mensajes de error de Liquibase, etc. never esconde la misma informacion del operador legitimo, que la necesita para diagnosticar.

when-authorized es el balance: K8s consulta el endpoint sin auth y solo recibe el status agregado (suficiente para los probes), mientras que un operador con un usuario valido ve los detalles que necesita. La misma logica aplica a show-components. La definicion de cada valor esta en la doc de Spring.

Por que health y los probes estan desacoplados

Por default, /actuator/health/readiness solo evalua readinessState, no los demas indicadores (datasources, bandejas). Es tentador agregar todos a la readiness — "si la base no responde, el pod no esta listo" — pero eso amplifica fallos transitorios en cascada:

  • Si la base de datos tiene un blip de 30 segundos, todas las replicas pasan a OUT_OF_SERVICE simultaneamente.

  • K8s detecta el outage de la app, restartea pods buscando uno saludable, y satura aun mas la base.

  • La aplicacion entera se vuelve indisponible por un fallo que era recuperable.

readinessState es un estado interno de la app — la app misma decide si esta lista, no lo deriva del estado de sus dependencias. Si una dependencia critica esta caida, lo apropiado es:

  • Devolver errores 5xx a los clientes (visible en metricas y logs).

  • Eventualmente, si la app considera que no puede operar en absoluto, emitir un AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC) desde codigo de la app — decision deliberada, no automatica.

El indicador de bandeja documenta este principio en su Javadoc:

Reports UP if the stats query succeeds (the binary signal K8s probes need), DOWN only if the query throws — i.e. the indicator is a probe of the read path, not of backlog.

Backlog y latencia se monitorean en metricas y alertas (Prometheus / Grafana), no en probes de K8s.

Por que tres capas para las bandejas

HealthIndicator + @Endpoint + Micrometer Gauge cubren tres consumidores con perfiles distintos:

Consumidor Pregunta que hace

K8s probes

"¿Puedo leer la bandeja?" — binario, sin detalle, sin historico. HealthIndicator con UP/DOWN.

Operador / SRE en debugging

"¿Cuantos mensajes tiene cada bandeja ahora?" — snapshot cuantitativo, accesible via curl. @Endpoint custom en /actuator/message-boxes.

Dashboard / alerta automatizada

"¿Como evoluciona el backlog en el tiempo?" — series temporales. Micrometer Gauge exportado a Prometheus / OTEL.

Una sola capa intentando servir a los tres falla:

  • Solo HealthIndicator con details: el operador puede leerlo, pero las series temporales no se pueden derivar (los detalles del health no se exportan como metrica).

  • Solo metrica: K8s no puede consumir Prometheus para sus probes.

  • Solo @Endpoint: ni K8s ni Prometheus saben hablar con un endpoint custom.

Las tres capas comparten implementacion via MessageBoxStatsView, asi que el costo de agregarlas es bajo.

Por que stuck-threshold con override por bandeja

Las cuatro bandejas tienen patrones de procesamiento distintos:

  • events.outbox: producido por commits de dominio, consumido por el dispatcher que publica al broker. Cadencia alta, latencia esperada baja (segundos).

  • commands.inbox: producido por mensajes externos, procesado en orden con locking distribuido. Cadencia variable, latencia tolerable mayor (decenas de segundos).

Un solo stuck-threshold global obliga a calibrar el peor caso (alto), perdiendo sensibilidad en las bandejas rapidas. Un threshold por bandeja sin default global obliga a configurar las cuatro siempre, aunque tres compartan valor.

La resolucion en cascada — override por bandeja → default global → fallback hardcoded de 5 minutos — combina lo mejor: un valor razonable funciona out of the box, y bajar la sensibilidad de una bandeja especifica es una sola linea de YAML.

Por que no hay management port separado

Una practica comun en despliegues sensibles es exponer los endpoints de actuator en un puerto distinto al puerto de aplicacion (server.port: 8080, management.server.port: 8081), para que el firewall / ingress pueda no exponer el segundo al exterior. El arquetipo no lo hace por defecto.

Razon: el modelo de despliegue asumido es Kubernetes con una Service por pod y NetworkPolicy controlando el acceso. La separacion de puertos no agrega aislamiento real cuando ambos puertos viven en el mismo pod — y agrega:

  • Un segundo Connector Tomcat (mas memoria, mas threads).

  • Configuracion adicional de Service y Ingress a mantener.

  • Confusion en debugging local (¿que puerto era el de actuator?).

La proteccion vive en otra capa: la allowlist de endpoints, show-details: when-authorized, y la NetworkPolicy / Ingress que restringe /actuator/** a clientes internos (operadores, scrapers de metricas). Si en un despliegue particular la separacion de puertos suma — porque la red no se puede segmentar — habilitarla en ese servicio especifico, no en el arquetipo.

Cache de endpoints

Las TTLs configuradas (10s en health, 300s en info) responden a dos patrones distintos:

  • health es consultado por K8s con cadencia alta — varias veces por segundo en un cluster con muchos pods. Sin cache, cada probe dispararia las queries de los indicadores (incluyendo las cuatro bandejas y los datasources). 10 s amortigua sin que el delay sea perceptible — un fallo se detecta en a lo sumo 10 s, lo cual es menor al periodSeconds tipico de los probes.

  • info contiene metadata estatica (build version, git commit). Recalcularla en cada hit es desperdicio puro. 300 s deja un margen amplio para reducir la carga.

Estas TTLs se ajustan si el monitoreo del servicio especifico lo justifica — los defaults son punto de partida, no inviolables.

Referencias