Spring Boot y Spring Cloud
Spring Boot Actuator y observabilidad: health, métricas, trazas y logs
Intermedio8 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Sabrás exponer exactamente los endpoints de management que necesita un servicio, configurar las probes de liveness y readiness, publicar métricas de aplicación y de negocio en Prometheus, propagar el contexto de traza entre servicios y hasta los logs, y blindar todo el conjunto para que la observabilidad no se convierta en una fuga de información.
Lo que aprenderás
- Habilitar y exponer los endpoints de Actuator de forma selectiva y protegerlos
- Configurar grupos de health y las probes de liveness y readiness de Kubernetes, y escribir un HealthIndicator propio
- Publicar métricas de la JVM, HTTP y de negocio a través de Micrometer hacia Prometheus
- Añadir tracing distribuido con Micrometer Tracing y correlacionar las trazas con las líneas de log
- Explicar la Observation API e instrumentar con ella una ruta de código crítica
Un servicio que no puedes observar es un servicio que no puedes operar. Spring Boot Actuator convierte un jar en algo a lo que un operador puede hacer preguntas: ¿está sano?, ¿qué está haciendo?, ¿a qué velocidad?, ¿y cuál fue la petición que falló a las 03:14? Esta página cubre los cuatro pilares tal como los implementa Boot 3: los endpoints de management, el health y las probes, las métricas con Micrometer y las trazas con Micrometer Tracing y la correlación de logs. Por el camino aborda la parte que la mayoría de los tutoriales se salta: no exponer todo eso a la Internet pública.
Endpoints: habilitar, exponer y proteger
Añade spring-boot-starter-actuator. Cada endpoint tiene dos interruptores independientes: habilitado (¿existe el bean?) y expuesto (¿es accesible por HTTP o JMX?). Todos los endpoints salvo shutdown están habilitados por defecto; por HTTP solo se expone health. La exposición se declara como una lista:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers
base-path: /actuator # valor por defecto; cámbialo si / ya lo ocupa un prefijo de API
endpoint:
health:
show-details: when-authorized
env:
show-values: when-authorized # Boot 3 enmascara por defecto todos los valores de env y configprops
server:
port: 9090 # opcional: tráfico de management en un puerto aparte
Los endpoints que conviene conocer:
| Endpoint | Qué te ofrece | Recomendación de exposición |
|---|---|---|
health | Estado agregado de la aplicación y sus dependencias | Público sin detalles; detalles para usuarios autenticados |
info | Versión del build, commit de git, datos personalizados | Se puede exponer internamente sin problema |
metrics / prometheus | Meters de Micrometer, en JSON o en formato de texto de Prometheus | Exponer solo al scraper |
loggers | Leer y cambiar niveles de log en caliente | Autenticado |
env, configprops | Configuración efectiva y propiedades enlazadas | Autenticado, con valores enmascarados |
beans, conditions, mappings | Introspección del contexto para depurar | Autenticado, o solo en local |
threaddump, heapdump | Diagnóstico bajo carga | Autenticado; los heap dumps contienen secretos |
shutdown | Parada ordenada mediante POST | Deshabilitado salvo que de verdad lo necesites |
Ejecutar los endpoints de management en un puerto aparte (management.server.port) es el endurecimiento más sencillo: el puerto principal se conecta al load balancer y el de management solo a la red del clúster. Cuando la aplicación usa Spring Security, los endpoints del puerto principal quedan protegidos por la misma filter chain, y los matchers de EndpointRequest te permiten escribir la regla con precisión:
@Bean
SecurityFilterChain actuatorSecurity(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
.anyRequest().hasRole("OPS"))
.httpBasic(Customizer.withDefaults());
return http.build();
}
El endpoint info está vacío hasta que lo alimentas. Los plugins de Maven y Gradle pueden generar información de build (el goal build-info de spring-boot-maven-plugin, springBoot { buildInfo() } en Gradle), el plugin git-commit-id añade los datos del commit y cualquier bean InfoContributor añade entradas personalizadas. Con info.env.enabled=true, las propiedades info.* también se convierten en entradas.
Health y probes
health agrega los beans HealthIndicator. Boot autoconfigura uno para cada dependencia que conoce: el DataSource, Redis, RabbitMQ, Kafka, Elasticsearch, el espacio en disco y más. Cada uno devuelve UP, DOWN, OUT_OF_SERVICE o UNKNOWN; el agregado es DOWN si lo es cualquier contribuidor, y el código HTTP es 503 para DOWN y OUT_OF_SERVICE, y 200 en los demás casos.
Un indicador propio es un bean pequeño:
@Component
class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient gateway;
PaymentGatewayHealthIndicator(PaymentGatewayClient gateway) {
this.gateway = gateway;
}
@Override
public Health health() {
try {
Duration latency = gateway.ping();
return Health.up().withDetail("latency", latency.toMillis() + "ms").build();
} catch (Exception ex) {
return Health.down(ex).build(); // el mensaje de la excepción solo aparece con show-details
}
}
}
El nombre del bean sin el sufijo HealthIndicator se convierte en la clave del contribuidor (paymentGateway). Mantén los indicadores ligeros: se ejecutan en cada probe y los orquestadores sondean a menudo. Cachea el resultado unos segundos si la comprobación implica una llamada de red.
Liveness y readiness
Kubernetes distingue dos preguntas, y tu configuración de health debería hacer lo mismo. Boot expone dos grupos de health, liveness y readiness, en /actuator/health/liveness y /actuator/health/readiness. Se habilitan automáticamente cuando la aplicación detecta que se ejecuta en Kubernetes (a través de la variable de entorno KUBERNETES_SERVICE_HOST), y se pueden forzar con management.endpoint.health.probes.enabled=true.
Por defecto, el grupo liveness solo contiene el LivenessStateHealthIndicator, que refleja el LivenessState propio de la aplicación y no toca sistemas externos. El grupo readiness solo contiene el ReadinessStateHealthIndicator, que pasa a REFUSING_TRAFFIC durante el arranque y la parada. Las dependencias que importan las añades explícitamente a readiness:
management:
endpoint:
health:
group:
readiness:
include: readinessState,db,paymentGateway
liveness:
include: livenessState
No pongas la base de datos en la liveness. Si la base de datos cae, reiniciar todos los pods no la va a recuperar; solo añade tormentas de reconexiones. La readiness es la palanca correcta: el pod deja de recibir tráfico y vuelve a incorporarse cuando la dependencia se recupera.
El fragmento de despliegue correspondiente:
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 9090 }
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 9090 }
periodSeconds: 5
El código de la aplicación puede cambiar los estados con AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC), por ejemplo mientras se calienta una caché. Con server.shutdown=graceful, Boot marca primero la readiness como «rechazando tráfico», espera a que terminen las peticiones en curso y luego se detiene, algo que encaja de forma natural con los rolling updates.
Métricas con Micrometer
Las métricas de Actuator se basan en Micrometer, una fachada neutral respecto al proveedor, al estilo de lo que es SLF4J para el logging. Boot registra meters para la JVM (memoria, garbage collection, threads, carga de clases), el sistema (CPU, descriptores de fichero), las peticiones del servidor HTTP, las llamadas de RestClient/WebClient, los pools de DataSource, las cachés, los executors y más. GET /actuator/metrics/http.server.requests muestra el meter con sus tags; profundiza con ?tag=uri:/api/orders&tag=status:500.
Para enviarlas a algún sitio, añade un registry. Para Prometheus:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
y expón prometheus; el scraper lee /actuator/prometheus en el formato de texto de exposición. Los registries para OpenTelemetry (OTLP), Datadog, New Relic, Graphite, InfluxDB y otros siguen el mismo patrón y pueden coexistir.
Es en las métricas de negocio donde la observabilidad se amortiza. Inyecta el MeterRegistry:
@Service
class CheckoutService {
private final Counter ordersPlaced;
private final Timer paymentTimer;
CheckoutService(MeterRegistry registry) {
this.ordersPlaced = Counter.builder("shop.orders.placed")
.description("Orders successfully placed")
.register(registry);
this.paymentTimer = Timer.builder("shop.payment.duration")
.publishPercentileHistogram() // permite que Prometheus calcule cuantiles
.register(registry);
}
Order checkout(Cart cart) {
Payment payment = paymentTimer.record(() -> gateway.charge(cart.total()));
Order order = orders.create(cart, payment);
ordersPlaced.increment();
return order;
}
}
Los tags añaden dimensiones (.tag("channel", "web")), y cada valor de tag distinto crea una nueva serie temporal; nunca etiquetes con valores no acotados como ids de cliente o texto libre. @Timed en un método funciona cuando TimedAspect está registrado como bean y AOP está en el classpath. Los tags comunes a todos los meters (management.metrics.tags.application=shop, region=eu-west-1) permiten filtrar los dashboards entre servicios.
Los percentiles merecen un apunte: publishPercentiles los calcula en la aplicación y no se pueden agregar entre instancias; publishPercentileHistogram publica buckets para que el sistema de monitorización calcule los cuantiles sobre toda la flota. Prefiere el histograma para todo lo que vayas a agregar.
Tracing y la Observation API
Las métricas te dicen que el percentil 99 se ha disparado; las trazas te dicen qué llamada aguas abajo lo provocó. Spring Boot 3 sustituyó Spring Cloud Sleuth por Micrometer Tracing, una fachada sobre una implementación de tracer. Elige un bridge y un exporter:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management:
tracing:
sampling:
probability: 0.1 # 0.1 por defecto; 1.0 en desarrollo
otlp:
tracing:
endpoint: http://otel-collector:4318/v1/traces
Con eso en marcha, las peticiones HTTP entrantes, las llamadas de RestClient/WebClient, JDBC (mediante la librería datasource-micrometer) y las interacciones con Kafka y RabbitMQ generan spans, y la cabecera W3C traceparent propaga el contexto entre servicios (B3 está disponible mediante management.tracing.propagation.type). micrometer-tracing-bridge-brave con Zipkin es la combinación alternativa.
El concepto que lo unifica todo por debajo es la Observation API de Micrometer 1.10. Una Observation es una unidad de trabajo que, al iniciarse y detenerse, produce un timer y un span y todo lo demás que hagan los handlers registrados. Instrumentar una sola vez tu propia ruta crítica te da ambas señales:
Observation.createNotStarted("shop.checkout", observationRegistry)
.lowCardinalityKeyValue("channel", cart.channel()) // se convierte en tag de métrica y en atributo del span
.highCardinalityKeyValue("cart.id", cart.id()) // solo en el span, nunca como tag de métrica
.observe(() -> checkoutService.checkout(cart));
@Observed en un método hace lo mismo de forma declarativa cuando ObservedAspect está registrado. La separación entre baja y alta cardinalidad codifica directamente en la API la regla de etiquetado de la sección de métricas.
Correlacionar logs con trazas
Cuando el tracing está activo, Boot 3 coloca traceId y spanId en el MDC de logging y los incluye en el patrón de consola por defecto como [app,traceId,spanId]. Copiar un trace id de una línea de log a tu interfaz de tracing, o de una traza a tu búsqueda de logs, cierra el círculo entre ambos sistemas. Si personalizas logging.pattern.console, mantén en él %X{traceId} y %X{spanId}, o usa logging.pattern.correlation (Boot 3.1+) para cambiar solo ese fragmento.
El logging estructurado remata el trabajo: Boot 3.4 puede escribir líneas JSON en formato Elastic Common Schema o Logstash con logging.structured.format.console=ecs, de modo que una plataforma de logs indexa los trace ids y los campos personalizados sin parsear con regex. Devolver el trace id a los clientes en las respuestas de error, como se sugiere en la guía de manejo de errores, convierte un ticket de soporte en una búsqueda directa.
Errores comunes
Exponer * y olvidarse. El management.endpoints.web.exposure.include=* de un tutorial deja env, heapdump y threaddump al alcance de cualquiera que encuentre /actuator. Enumera los endpoints que necesitas y pon el resto detrás de autenticación o de un puerto aparte.
Meter dependencias externas en la probe de liveness. Una caída de la base de datos se convierte en un bucle de reinicios en toda la flota. La liveness se queda en lo interno; la readiness carga con las dependencias.
Health checks costosos. Un indicador que lanza una consulta sobre una tabla grande, o que llama a una API de terceros sin timeout, convierte la propia probe en un problema de carga. Pon un timeout a cada comprobación y cachea los resultados.
Tags de alta cardinalidad. Un tag userId en http.server.requests crea una serie temporal por usuario y puede agotar tanto el registry como el backend de monitorización. Usa los valores de alta cardinalidad solo en los spans.
Muestrear al 100 % en producción sin planificar la capacidad. Las trazas ocupan mucho. Empieza con la probabilidad por defecto y súbela para servicios concretos o durante incidentes, o usa tail-based sampling en el collector.
Dar por hecho que las métricas aparecen sin registry. El endpoint metrics muestra los meters, pero no se exporta nada hasta que añades una dependencia de registry; el endpoint prometheus no existe sin micrometer-registry-prometheus.
Puntos clave
- Los endpoints de Actuator están habilitados por defecto, pero solo
healthse expone por HTTP; expón una lista explícita, protege el resto y plantéate un puerto de management aparte. - La liveness debe reflejar solo el estado propio del proceso; la readiness incluye las dependencias que necesita una petición, de modo que los fallos desvían tráfico en lugar de reiniciar pods.
- Micrometer ofrece de serie métricas de la JVM, HTTP y de pools; añade un registry (Prometheus, OTLP u otros) para exportarlas y unos cuantos meters de negocio bien etiquetados para que tengan sentido.
- Micrometer Tracing sustituye a Sleuth en Boot 3; elige un bridge (OpenTelemetry o Brave) y un exporter, fija la probabilidad de muestreo y el contexto se propaga automáticamente.
- La Observation API instrumenta una ruta de código una sola vez y produce tanto un timer como un span; mantén los valores de alta cardinalidad fuera de los tags de métricas.
- Los trace ids en el MDC y los logs JSON estructurados enlazan logs, métricas y trazas en una única vía de investigación.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Qué endpoints de Actuator se exponen por HTTP por defecto?
- Solo health. Todos los demás endpoints (info, metrics, env, beans, loggers, threaddump, etc.) están habilitados pero no se exponen en la web hasta que los incluyes en management.endpoints.web.exposure.include. Por JMX, la exposición por defecto también se limita a health desde Boot 2.2. Este valor por defecto tan conservador existe porque endpoints como env y heapdump revelan la configuración y el contenido de la memoria.
- ¿Qué diferencia hay entre liveness y readiness?
- La liveness responde a «¿está este proceso roto sin remedio?», y si la probe falla Kubernetes reinicia el contenedor, así que no debe depender de sistemas externos. La readiness responde a «¿puede esta instancia recibir tráfico ahora mismo?», y si falla solo se retira el pod de los endpoints del Service, por lo que es el lugar adecuado para reflejar una conexión perdida con la base de datos o una fase de calentamiento.
- ¿Sigo necesitando Spring Cloud Sleuth para el tracing?
- No. Spring Cloud Sleuth llegó al final de su vida con Spring Boot 2.x. En Boot 3 el tracing lo proporciona Micrometer Tracing, con bridges hacia OpenTelemetry o Brave, y Actuator lo autoconfigura. La API cambió (Observation y Tracer en lugar de los tipos Span de Sleuth), pero el prefijo de propiedades management.tracing.* cubre el muestreo y la propagación.
- ¿Cómo cambio un nivel de log en producción sin reiniciar?
- Expón el endpoint loggers y envía un POST a /actuator/loggers/com.example.shop con un cuerpo JSON como {"configuredLevel": "DEBUG"}. El cambio es inmediato y se pierde al reiniciar. Protege el endpoint con autenticación, ya que puede generar suficiente volumen de logs como para afectar al servicio.