Observabilidad en microservicios Spring Boot: logs, métricas y trazas

Capítulo 7

Tiempo estimado de lectura: 13 minutos

+ Ejercicio

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 traceId y 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:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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: prod

Propagar 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/spans

Con 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-servicepayment-serviceinventory-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-service con su duración.
  • Dentro de payment-service: span servidor (entrada) y spans hijos (DB, proveedor externo).
  • Span hijo adicional desde order-service a inventory-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 service desde spring.application.name.
  • Define environment desde configuración.
  • Activa trazas y asegúrate de que traceId/spanId llegan 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/prometheus y /actuator/health con 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 probability baja (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é observarCómo hacerloQué concluye
Métrica http.server.requests por endpoint (p95/p99)Filtra por uri y status; compara con baselineIdentifica endpoints responsables y si afecta a éxitos o errores
Trazas de requests lentasBusca spans con mayor duración; revisa el “critical path”Ubica si el tiempo se va en dependencia externa, DB o CPU
Pool de conexiones/threadsMétricas de HikariCP, threads, GCSaturación produce colas y latencia sin necesariamente aumentar errores
Logs WARN de degradaciónFiltra por traceId de trazas lentas y por eventos de reintento/circuit breakerReintentos 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ñalQué buscarAcción inmediata
Errores tipo timeoutContadores por excepción (SocketTimeoutException, ReadTimeout) y logs WARN/ERROR con traceIdCorrelaciona con trazas para ver en qué dependencia ocurre
Spans de cliente con duración cercana al timeoutEn la traza, spans de llamada saliente que terminan en errorDetermina si el destino no responde o si hay congestión
Readiness/health del servicio destino/actuator/health y métricas de saturaciónSi 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ótesisCómo se veCómo confirmarla
Dependencia inestableErrores agrupados por destino; spans fallidos en trazasCorrelaciona traceId y revisa health/latencia del destino
Condición de carrera / recursos compartidosErrores raros, no reproducibles; picos con cargaRevisa métricas de threads, locks, pool; habilita DEBUG temporal en componente
Despliegues/configuraciónErrores coinciden con cambios de versiónEtiqueta logs/métricas con version (tag) y compara por release
Datos específicosFalla con ciertos inputsBusca 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 traceId representativos de fallos y compáralos con trazas exitosas.
  • 3) En logs JSON, filtra por traceId y 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.

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la ventaja principal de definir un contrato común de señales (campos en logs, métricas mínimas y propagación de contexto) en un sistema de microservicios?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Un contrato común estandariza campos y tags (por ejemplo, service, environment, traceId/spanId), lo que permite correlacionar logs, métricas y trazas entre servicios y reducir el tiempo de diagnóstico.

Siguiente capítulo

Configuración externa y gestión de entornos en microservicios con Spring Boot

Arrow Right Icon
Portada de libro electrónico gratuitaMicroservicios con Spring Boot desde Cero
58%

Microservicios con Spring Boot desde Cero

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.