Enfoque de observabilidad: qué medir y por qué
En un sistema de microservicios, la observabilidad es la capacidad de entender qué está pasando (y por qué) a partir de señales producidas por el sistema. En la práctica se apoya en tres pilares complementarios: logs (qué ocurrió), métricas (cuánto y con qué tendencia) y trazas (dónde se gastó el tiempo a través de servicios). El objetivo operativo es reducir el tiempo de diagnóstico: detectar rápido, acotar el problema y confirmar la causa.
Un enfoque útil es definir primero un contrato de señales común para todos los servicios: campos obligatorios en logs, métricas mínimas por endpoint/dependencia y un estándar de propagación de contexto (traceId/spanId y, cuando aplique, userId). Esto permite correlacionar eventos entre servicios y construir paneles/alertas consistentes.
Logging estructurado (JSON): diseño, niveles y campos obligatorios
Qué es logging estructurado y por qué usar JSON
Logging estructurado significa emitir logs como eventos con campos (clave/valor) en lugar de texto libre. Con JSON, herramientas de búsqueda y agregación pueden filtrar por service, environment, traceId o http.status sin depender de expresiones regulares frágiles. Además, facilita métricas derivadas (por ejemplo, contar errores por endpoint) y correlación con trazas.
Campos obligatorios recomendados
Define un conjunto mínimo de campos que todo log debe incluir. Recomendación práctica:
timestamp: lo añade el appender/encoder.level: INFO/WARN/ERROR/DEBUG.service: nombre del microservicio (estable).environment: dev/stage/prod (o similar).traceId: identificador de traza distribuida.spanId: identificador del segmento actual (opcional pero útil).userId: cuando aplique (peticiones autenticadas); evita PII.event: nombre corto del evento (por ejemplo,payment_authorized).message: texto humano breve.error.type,error.message,error.stack: solo en errores.http.method,http.path,http.status,durationMs: para logs de request/response si decides registrarlos.
Niveles de log: reglas operativas
- ERROR: fallos que requieren atención (excepciones no controladas, dependencia caída, corrupción de datos). Siempre con
traceIdy detalles de error. - WARN: comportamiento inesperado pero recuperable (reintentos, degradación, timeouts puntuales).
- INFO: eventos de negocio y ciclo de vida (inicio, configuración relevante, cambios de estado, resultados de procesos).
- DEBUG/TRACE: diagnóstico detallado; en producción, habilitar temporalmente y preferir muestreo o scopes acotados.
Implementación en Spring Boot: JSON con Logback
Una forma común es usar logstash-logback-encoder para emitir JSON. Añade la dependencia:
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version></dependency>Configura logback-spring.xml para incluir campos estáticos (service, environment) y el contexto de trazas (MDC):
<configuration> <springProperty scope="context" name="SERVICE" source="spring.application.name"/> <springProperty scope="context" name="ENV" source="app.environment" defaultValue="dev"/> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <timestamp/> <logLevel/> <threadName/> <loggerName/> <message/> <mdc/> <stackTrace/> <globalCustomFields> {"service":"${SERVICE}","environment":"${ENV}"} </globalCustomFields> </providers> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT"/> </root></configuration>En application.yml define el entorno:
app: environment: prodPropagar userId y otros campos con MDC
Para que traceId y spanId aparezcan en MDC, lo habitual es usar instrumentación de trazas (ver sección de trazabilidad). Para userId, puedes poblar MDC en un filtro una vez autenticado el usuario. Ejemplo (simplificado) con un OncePerRequestFilter:
@Componentpublic class UserMdcFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String userId = request.getHeader("X-User-Id"); // o extraído del contexto de seguridad if (userId != null && !userId.isBlank()) { MDC.put("userId", userId); } filterChain.doFilter(request, response); } finally { MDC.remove("userId"); } }}Regla importante: no registres tokens, contraseñas ni datos sensibles. Si necesitas correlación, usa identificadores opacos.
Métricas de aplicación: latencia, throughput, errores y health checks
Qué métricas mínimas debes tener
Para operaciones, un set mínimo por servicio suele incluir:
- Latencia: p50/p95/p99 por endpoint y por dependencia (DB, HTTP a otros servicios).
- Throughput: requests por segundo (RPS) por endpoint.
- Errores: tasa de 4xx/5xx, excepciones por tipo, timeouts.
- Recursos: CPU, memoria, GC, threads, pool de conexiones.
- Dependencias: latencia y errores de llamadas salientes (HTTP/DB/mensajería).
Micrometer + Actuator: instrumentación base
En Spring Boot, la base práctica es Spring Boot Actuator + Micrometer. Añade dependencias:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId></dependency>Configura endpoints y tags comunes:
management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: probes: enabled: true metrics: tags: service: ${spring.application.name} environment: ${app.environment}Con esto obtienes métricas HTTP estándar como http.server.requests (latencia y conteos) y un endpoint /actuator/prometheus para scraping.
Métricas personalizadas (negocio y dependencias)
Además de lo automático, instrumenta puntos críticos. Ejemplo: medir el tiempo de un proceso de negocio y contar resultados:
@Servicepublic class CheckoutService { private final MeterRegistry registry; public CheckoutService(MeterRegistry registry) { this.registry = registry; } public void checkout(String orderId) { Timer.Sample sample = Timer.start(registry); try { // lógica registry.counter("checkout_total", "result", "success").increment(); } catch (Exception e) { registry.counter("checkout_total", "result", "error", "exception", e.getClass().getSimpleName()).increment(); throw e; } finally { sample.stop(Timer.builder("checkout_latency") .publishPercentileHistogram() .register(registry)); } }}Recomendación: limita la cardinalidad de etiquetas (tags). Evita tags con valores infinitos como userId o orderId en métricas.
Health checks útiles para operaciones
Un health útil no solo dice “UP/DOWN”, sino que permite distinguir:
- Liveness: el proceso está vivo (no bloqueado).
- Readiness: puede recibir tráfico (dependencias listas, warm-up completado).
- Dependencias: DB, broker, servicios externos (con timeouts cortos).
Actuator soporta probes. Para checks personalizados:
@Componentpublic class DownstreamHealthIndicator implements HealthIndicator { private final RestClient restClient; public DownstreamHealthIndicator(RestClient.Builder builder) { this.restClient = builder.baseUrl("http://inventory").build(); } @Override public Health health() { try { long start = System.currentTimeMillis(); restClient.get().uri("/actuator/health").retrieve().toBodilessEntity(); long ms = System.currentTimeMillis() - start; return Health.up().withDetail("inventory", "UP").withDetail("latencyMs", ms).build(); } catch (Exception e) { return Health.down(e).withDetail("inventory", "DOWN").build(); } }}Buenas prácticas: usa timeouts agresivos en health checks, no ejecutes queries pesadas y evita cascadas (si un servicio depende de otro que depende de otro, no conviertas el health en un “árbol” lento).
Trazabilidad distribuida: generación, propagación e interpretación
Conceptos: trace, span y contexto
Una traza representa el recorrido completo de una solicitud a través de múltiples servicios. Está compuesta por spans (segmentos) que miden operaciones individuales: una entrada HTTP, una llamada a otro servicio, una consulta a base de datos. Cada span tiene un traceId común y un spanId propio; la relación padre-hijo permite reconstruir el árbol/flujo.
Cómo se genera y propaga el identificador de traza
En el borde (API Gateway o primer servicio que recibe la petición), si no existe un contexto de trazas entrante, se genera un nuevo traceId. Ese contexto se propaga a llamadas salientes (HTTP/mensajería) mediante headers estándar (por ejemplo W3C Trace Context: traceparent). Cada servicio crea spans hijos para sus operaciones internas y reenvía el contexto a los siguientes servicios.
Instrumentación práctica en Spring Boot (Micrometer Tracing)
En versiones modernas de Spring Boot, Micrometer Tracing reemplaza a Spring Cloud Sleuth. Añade dependencias para trazas y exportación (ejemplo con Zipkin):
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId></dependency><dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId></dependency>Configura el endpoint del colector y el muestreo:
management: tracing: sampling: probability: 0.1 zipkin: tracing: endpoint: http://zipkin:9411/api/v2/spansCon esto, Spring instrumenta automáticamente entradas HTTP y clientes HTTP comunes, y coloca traceId/spanId en MDC para que aparezcan en logs (si tu encoder incluye <mdc/>).
Propagación entre servicios: ejemplo mental de flujo
Supón un flujo order-service → payment-service → inventory-service. En una traza típica verás:
- Span raíz:
order-service(HTTP POST /orders) con duración total. - Span hijo: llamada HTTP a
payment-servicecon su duración. - Dentro de
payment-service: span servidor (entrada) y spans hijos (DB, proveedor externo). - Span hijo adicional desde
order-serviceainventory-service.
Interpretación práctica: si la latencia total es alta, busca el span más largo. Si el span largo es una llamada saliente, el problema puede estar en el servicio destino o en la red; si es una operación interna (DB), revisa consultas, pool o locks.
Correlación logs ↔ trazas
La regla operativa es: todo log relevante debe incluir traceId. Así, cuando encuentres un error en logs, puedes abrir la traza completa usando el traceId y ver el árbol de spans para ubicar el cuello de botella. A la inversa, desde una traza lenta puedes filtrar logs por traceId para ver excepciones, warnings o decisiones de negocio.
Guía práctica paso a paso: implementar observabilidad consistente
Paso 1: estandariza tags y campos
- Define
servicedesdespring.application.name. - Define
environmentdesde configuración. - Activa trazas y asegúrate de que
traceId/spanIdllegan al MDC. - Decide cómo poblar
userId(solo cuando aplique) y en qué capa (filtro).
Paso 2: configura logs JSON en todos los servicios
- Usa el mismo encoder JSON y el mismo set de campos globales.
- Define políticas de niveles: root INFO, paquetes propios INFO/DEBUG según necesidad, y eleva a WARN/ERROR en fallos.
- Evita logs de request/response completos en producción; si los necesitas, redáctalos (masking) y limita tamaño.
Paso 3: expón métricas y health checks
- Activa
/actuator/prometheusy/actuator/healthcon probes. - Agrega tags comunes (
service,environment). - Instrumenta métricas personalizadas en procesos críticos.
- Revisa cardinalidad: endpoints con path templated (por ejemplo
/orders/{id}) deben agregarse correctamente.
Paso 4: habilita trazas con muestreo
- Empieza con
probabilitybaja (p. ej. 0.1) en producción. - Para incidentes, incrementa temporalmente el muestreo o aplica sampling basado en reglas (si tu stack lo soporta).
- Asegura propagación de contexto en clientes HTTP y mensajería (si usas colas/eventos, propaga headers).
Diagnóstico por escenarios (runbook)
Escenario A: aumento de latencia (p95/p99 sube)
| Qué observar | Cómo hacerlo | Qué concluye |
|---|---|---|
Métrica http.server.requests por endpoint (p95/p99) | Filtra por uri y status; compara con baseline | Identifica endpoints responsables y si afecta a éxitos o errores |
| Trazas de requests lentas | Busca spans con mayor duración; revisa el “critical path” | Ubica si el tiempo se va en dependencia externa, DB o CPU |
| Pool de conexiones/threads | Métricas de HikariCP, threads, GC | Saturación produce colas y latencia sin necesariamente aumentar errores |
| Logs WARN de degradación | Filtra por traceId de trazas lentas y por eventos de reintento/circuit breaker | Reintentos multiplican latencia y carga |
Pasos prácticos:
- 1) Confirma si el aumento es global o por endpoint específico (métricas).
- 2) Toma una muestra de trazas lentas y localiza el span más largo.
- 3) Si el span largo es una llamada HTTP a otro servicio: revisa métricas del servicio destino (latencia/errores) y su salud.
- 4) Si el span largo es DB: revisa métricas de pool (conexiones activas/pendientes), tiempos de query y locks; correlaciona con logs de slow queries si existen.
- 5) Si no hay span largo claro pero la traza completa es lenta: sospecha colas (threads), GC o backpressure; revisa métricas de JVM y saturación.
Escenario B: timeouts en llamadas entre servicios
| Señal | Qué buscar | Acción inmediata |
|---|---|---|
| Errores tipo timeout | Contadores por excepción (SocketTimeoutException, ReadTimeout) y logs WARN/ERROR con traceId | Correlaciona con trazas para ver en qué dependencia ocurre |
| Spans de cliente con duración cercana al timeout | En la traza, spans de llamada saliente que terminan en error | Determina si el destino no responde o si hay congestión |
| Readiness/health del servicio destino | /actuator/health y métricas de saturación | Si no está ready, corta tráfico o escala |
Pasos prácticos:
- 1) Identifica el origen: ¿qué servicio está experimentando timeouts? (métricas de errores y logs).
- 2) Con el
traceId, abre la traza y ubica el span de cliente que falla; anota destino, ruta y duración. - 3) Revisa en el servicio destino si hay aumento de latencia o saturación (threads/pool/CPU).
- 4) Si el destino está sano, revisa red/DNS/balanceador y límites de conexión (por ejemplo, pool de HTTP cliente agotado).
- 5) Reduce impacto: baja concurrencia, ajusta timeouts/reintentos (evita reintentos agresivos), y considera degradación controlada.
Escenario C: errores intermitentes (picos de 5xx/4xx inesperados)
| Hipótesis | Cómo se ve | Cómo confirmarla |
|---|---|---|
| Dependencia inestable | Errores agrupados por destino; spans fallidos en trazas | Correlaciona traceId y revisa health/latencia del destino |
| Condición de carrera / recursos compartidos | Errores raros, no reproducibles; picos con carga | Revisa métricas de threads, locks, pool; habilita DEBUG temporal en componente |
| Despliegues/configuración | Errores coinciden con cambios de versión | Etiqueta logs/métricas con version (tag) y compara por release |
| Datos específicos | Falla con ciertos inputs | Busca patrones en logs estructurados (sin PII) y agrega event + códigos de error |
Pasos prácticos:
- 1) Segmenta el error: por endpoint, por instancia, por versión, por dependencia (métricas con tags).
- 2) Toma 5–10
traceIdrepresentativos de fallos y compáralos con trazas exitosas. - 3) En logs JSON, filtra por
traceIdy busca el primer WARN/ERROR; identifica el evento y el componente. - 4) Si el error es intermitente y no deja rastro, incrementa muestreo de trazas y habilita logs DEBUG solo en el paquete afectado por una ventana corta.
- 5) Añade métricas/contadores específicos del fallo (por ejemplo,
downstream_error_total{service="inventory",code="..."}) para detectar recurrencia.