StackPractices
intermediate Por Mathias Paulenko

Guía de Logging, Monitoreo y Observabilidad

Guía para construir sistemas observables con logging estructurado, métricas y tracing distribuido.

Introducción

Cuando algo se rompe en producción, la primera pregunta es siempre la misma: ¿por qué? Sin observabilidad, responderla implica entrar por SSH a un servidor, hacer grep sobre logs de texto y adivinar. Con observabilidad, abres un dashboard, pegas un trace ID y ves exactamente dónde la petición se demoró o falló.

La observabilidad es la capacidad de entender el estado interno de un sistema examinando sus salidas. No es una herramienta que se instala — es una propiedad que se construye en el sistema mediante tres señales complementarias: logs (qué pasó), métricas (cómo va la tendencia) y traces (dónde se fue el tiempo). Cada una responde una pregunta distinta, y el valor está en correlacionarlas: un pico en una métrica de errores te lleva a un trace, y el ID de ese trace te lleva a los logs que explican el fallo.

Esta guía cubre el lado práctico: cómo estructurar logs, qué métricas recolectar, cómo instrumentar traces con OpenTelemetry, cómo alertar sobre SLOs en lugar de CPU, y cómo encajan las piezas en un sistema real de microservicios. Un buen runbook complementa todo esto — mira la Plantilla de Runbook para el lado operativo.

¿Para quién es esto? Ingenieros que operan un servicio en producción y quieren observabilidad que realmente acorte los incidentes — no un muro de dashboards que nadie lee. No necesitas un stack específico: todo aquí usa OpenTelemetry, Prometheus y formatos abiertos que funcionan con cualquier backend, self-hosted o gestionado.

Los Tres Pilares

PilarPreguntaGranularidadRetención
Logs¿Qué pasó?Alta (eventos individuales)Días a semanas
Métricas¿Cómo va la tendencia?Baja (agregada)Meses a años
Traces¿Dónde se fue el tiempo?Media (rutas de peticiones)Días a semanas

Ningún pilar reemplaza a los otros. Las métricas son baratas de almacenar y rápidas de consultar, pero vienen pre-agregadas — no puedes profundizar en una petición concreta. Los logs guardan cada detalle, pero son caros de almacenar y lentos de buscar a escala. Los traces quedan en el medio: un registro por petición, con el tiempo de cada paso.

El pegamento es el ID de correlación (normalmente el trace ID). Estámpalo en cada línea de log, inclúyelo en las respuestas de error y expónlo como label de métrica donde la cardinalidad lo permita. Con ese único ID puedes saltar de una alerta a un trace y del trace a las líneas de log exactas de la petición fallida.

Logging Estructurado

Reemplaza el texto libre por JSON parseable por máquinas. "Pago fallido para el usuario 456" te obliga a escribir regexes para siempre; {"level": "error", "user_id": "user_456", "error": "Tarjeta rechazada"} es una consulta. Mira Logging Estructurado para la receta completa.

Formato

Cada entrada de log debería llevar un conjunto consistente de campos:

{
  "timestamp": "2026-06-11T14:32:01Z",
  "level": "ERROR",
  "message": "Pago fallido",
  "service": "billing-api",
  "trace_id": "abc123",
  "user_id": "user_456",
  "amount": 99.99,
  "error": "Tarjeta rechazada",
  "duration_ms": 245
}

Los imprescindibles: timestamp (ISO-8601, UTC), level, service y trace_id. Todo lo demás es contexto — agrega lo que querrías tener a las 3 de la mañana depurando un incidente.

Implementación (Python, structlog)

import structlog
import logging

structlog.configure(
    processors=[
        structlog.stdlib.filter_by_level,
        structlog.stdlib.add_logger_name,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.JSONRenderer()
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
)

logger = structlog.get_logger()
logger.info("payment_processed", user_id="123", amount=49.99)

Niveles de Log

NivelCaso de usoEjemplo
DEBUGDetalle de desarrolloValores de variables, iteraciones de bucles
INFOOperaciones normalesPetición completada, job iniciado
WARNInesperado pero manejadoReintento realizado, API obsoleta usada
ERROROperación fallidaPetición fallida, excepción capturada
FATALIndisponibilidad del sistemaConexión a base de datos perdida

Dos reglas prácticas: tener todo en INFO es tan inútil como no loguear nada, y ERROR debería significar “probablemente un humano tiene que mirar esto”. Si ERROR se dispara constantemente y nadie actúa, la asignación de niveles está mal.

Sampling para servicios de alto volumen

Un servicio con 50K peticiones por minuto no puede permitirse loguear cada petición con detalle completo. Muestrea por resultado: guarda el 100% de errores y peticiones lentas, y 1–5% de las exitosas. La mayoría de backends (Loki, Datadog, CloudWatch) admiten reglas de sampling en origen; la receta de análisis de archivos de log muestra el otro extremo — consultar lo que ya está almacenado.

Métricas

Las métricas son puntos de datos numéricos recolectados en el tiempo. A diferencia de los logs, se agregan muy bien: rate(http_requests_total{status="500"}[5m]) funciona igual con 10 o con 10 millones de peticiones por minuto.

Tipos de Métricas

TipoDescripciónEjemplo
CounterSolo aumentaPeticiones servidas, errores ocurridos
GaugePuede subir o bajarTamaño actual de cola, uso de memoria
HistogramDistribución de valoresDuración de petición, tamaño de payload
SummaryPercentiles calculadosLatencia p95, latencia p99

Prefiere histogramas sobre summaries cuando vayas a agregar entre instancias — Prometheus puede calcular percentiles a partir de los buckets del histograma entre muchos pods, mientras que los percentiles de un summary no se pueden fusionar.

Implementación (Prometheus, Python)

from prometheus_client import Counter, Histogram, start_http_server

requests_total = Counter('http_requests_total', 'Total requests', ['method', 'status'])
request_duration = Histogram('http_request_duration_seconds', 'Request duration')

@request_duration.time()
def handle_request():
    requests_total.labels(method='GET', status='200').inc()
    # ... procesar la petición

start_http_server(8000)  # Expone /metrics

La trampa de la cardinalidad

Los labels multiplican tu cantidad de series. user_id como label en un counter crea una serie temporal por usuario — con un millón de usuarios, el backend de métricas se cae. Mantén la cardinalidad baja (decenas, no miles): status, method, endpoint están bien; user_id, email, request_id no. Los identificadores de alta cardinalidad pertenecen a logs y traces.

Qué métricas recolectar primero

Dos marcos cubren la mayoría de los casos. Para servicios, usa el método RED: Rate (peticiones/segundo), Errors (peticiones fallidas/segundo), Duration (distribución de latencia). Tres métricas por endpoint te dan un dashboard funcional y cubren la mayoría de las condiciones de alerta. Para recursos, usa el método USE: Utilization (utilización), Saturation (saturación), Errors (errores) — qué tan ocupado está el recurso, cuánto trabajo tiene en cola y si está fallando.

Mira la receta de monitoreo y alertas con Prometheus para la configuración de scrape y ejemplos de reglas.

Tracing Distribuido

Los traces siguen una petición a través de múltiples servicios. Un trace es un árbol de spans — uno por unidad de trabajo — que comparten un trace ID común y se enlazan por parent span IDs.

Trace ID: abc123
├── Service A: 5ms  (petición HTTP recibida)
├── Service B: 12ms (verificación de auth)
├── Service C: 45ms (consulta a base de datos)
│   ├── Adquisición de conexión: 2ms
│   ├── Ejecución de query: 30ms
│   └── Mapeo de resultados: 13ms
└── Service D: 8ms  (formato de respuesta)

Esa sola vista ya te dice que los 45ms están en la query del Service C — no “algo anda lento por ahí”. La guía de tracing distribuido profundiza en propagación y elección de backend.

Implementación (OpenTelemetry, Python)

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

span_processor = BatchSpanProcessor(OTLPSpanExporter())
trace.get_tracer_provider().add_span_processor(span_processor)

with tracer.start_as_current_span("process_payment") as span:
    span.set_attribute("payment.amount", 99.99)
    span.set_attribute("payment.currency", "USD")
    # ... lógica de negocio

OpenTelemetry es el estándar en el que apostar: una sola capa de instrumentación que puede exportar a Jaeger, Tempo, Zipkin o al backend de un vendor sin cambios de código. Usa BatchSpanProcessor en producción (almacena y envía spans de forma asíncrona); el procesador simple exporta de forma síncrona y agrega latencia al camino de la petición.

Para que el contexto de trace se propague entre servicios, la llamada HTTP/gRPC saliente debe inyectar los headers de trace (traceparent, tracestate) y el servicio receptor debe extraerlos. La auto-instrumentación de OTel maneja esto para los frameworks y clientes HTTP comunes — esa es la razón principal para preferirlo sobre middleware hecho a mano.

Alertas

Alerta sobre síntomas, no causas. “p99 de latencia por encima de 1s durante 10 minutos” despierta a alguien; “CPU al 80%” probablemente no — la máquina puede estar bien haciendo su trabajo.

Reglas de Alerta

# Regla de alerta de Prometheus
groups:
  - name: api_alerts
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Alta tasa de errores en {{ $labels.service }}"

La cláusula for: 2m importa más de lo que parece: filtra picos momentáneos y es la defensa más barata contra la fatiga de alertas.

Niveles de Severidad de Alertas

SeveridadTiempo de respuestaEjemplo
CríticaInmediato (page)Servicio caído, riesgo de pérdida de datos, SLO quemándose rápido
WarningDentro de 1 horaTasa de error elevada, alta latencia
InfoPróximo día hábilCapacidad cercana al límite, certificado por expirar en 30d

Cada alerta debería responder tres preguntas: ¿hay un usuario afectado ahora mismo?, ¿puede un humano hacer algo al respecto?, ¿la anotación enlaza al runbook? Si alguna respuesta es no, debería ser un ticket o un panel de dashboard, no una página.

Por último, dale un responsable a cada alerta. Una alerta “crítica” enrutada a un canal que nadie mira es peor que no tener alerta — entrena al equipo a ignorar el canal.

Costo, Retención y Sampling

Las facturas de observabilidad escalan con el volumen, y el volumen crece más rápido que el tráfico a medida que añades servicios. Algunos números a tener en cuenta:

  • Los logs dominan el costo. Loguear todo a 500K peticiones/minuto son terabytes por semana. Combina retención corta para DEBUG/INFO (7 días) con retención más larga para ERROR y auditoría (30–90 días), y muestrea las peticiones exitosas en lugar de guardarlas todas.
  • Las métricas son baratas por punto de datos pero explotan con la cardinalidad. Diez labels con valores sin límite pueden producir más series que un año de tráfico normal. Acota los valores de los labels y elimina las métricas que nadie usa.
  • Los traces son el punto medio. El sampling en origen (guardar un N% de todos los traces) es simple pero descarta las peticiones interesantes; el tail-based sampling en el OTel Collector conserva todos los errores más una fracción de los éxitos, normalmente el default correcto.

Regla práctica de presupuesto: si la observabilidad cuesta más del 5–10% de tu factura de cómputo, algo está mal configurado — casi siempre volumen de logs o cardinalidad de métricas.

SLOs y Presupuestos de Error

Un SLO es un objetivo medible de confiabilidad: “el 99.9% de las peticiones tienen éxito en menos de 500ms en una ventana móvil de 30 días”. La brecha entre tu SLO y el 100% es el presupuesto de error (error budget) — el 0.1% de las peticiones, o 43.2 minutos de caída por mes.

El presupuesto convierte la confiabilidad en una herramienta de gestión. ¿Queda presupuesto? Despliega más rápido, toma riesgos, corre experimentos. ¿Presupuesto quemado? Congela features y arregla la confiabilidad. Es la respuesta más honesta a “¿podemos desplegar esta semana?”.

Alertas por burn rate

El burn rate mide qué tan rápido consumes el presupuesto. Si el SLO permite 0.1% de errores y actualmente estás en 1.4%, estás quemando a 14x — todo el presupuesto del mes en unas 2 horas. El SRE Workbook de Google recomienda alertas en dos niveles: pagear ante quema rápida (14x en 1 hora, presupuesto por agotarse) y ticket ante quema lenta (2x en 3 días, problema crónico).

- alert: SloBurnRateFast
  expr: rate(http_requests_total{status=~"5.."}[1h]) > 14.4 * 0.001
  labels:
    severity: critical
- alert: SloBurnRateSlow
  expr: rate(http_requests_total{status=~"5.."}[6h]) > 2 * 0.001
  labels:
    severity: warning

Escenario Detallado: Observabilidad para Microservicios de E-commerce

Juntándolo todo: una plataforma e-commerce de 15 servicios a 500K peticiones por minuto. Los pagos atraviesan checkout → payment-service → gateway, y cada salto es un span. Cada línea de log lleva el traceId, así que cuando la métrica payments.errors se dispara, el on-call puede buscar ese ID en Loki para el contexto del log y en Jaeger para la ruta completa de la petición — tres consultas, un incidente, minutos en lugar de horas.

Sistema: 15 microservicios, 500K peticiones/min
Stack: OpenTelemetry -> Jaeger (traces), Prometheus (métricas), Loki (logs)

Instrumentación (Node.js):
  const { trace, metrics } = require("@opentelemetry/api");
  const tracer = trace.getTracer("payment-service");
  const meter = metrics.getMeter("payment-service");
  const paymentsTotal = meter.createCounter("payments.total");
  const paymentsErrors = meter.createCounter("payments.errors");

  async function processPayment(payment) {
    const span = tracer.startSpan("processPayment");
    span.setAttribute("payment.amount", payment.amount);
    span.setAttribute("payment.currency", payment.currency);
    try {
      const result = await gateway.charge(payment);
      span.setAttribute("payment.status", result.status);
      paymentsTotal.add(1, { status: result.status, gateway: "stripe" });
      return result;
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: 2, message: error.message });
      paymentsErrors.add(1, { type: error.constructor.name });
      throw error;
    } finally {
      span.end();
    }
  }

Logs estructurados (JSON):
  {
    "timestamp": "2026-01-15T10:30:00Z",
    "level": "error",
    "service": "payment-service",
    "traceId": "abc123",
    "spanId": "def456",
    "message": "Pago fallido",
    "paymentId": "pay_789",
    "amount": 99.99,
    "currency": "USD",
    "error": "InsufficientFunds"
  }

  // Correlación: traceId conecta logs, métricas y traces
  // Buscar traceId en Loki -> ver todos los logs de la petición
  // Buscar traceId en Jaeger -> ver el trace completo

Dashboard de SLOs:
  | SLO | Objetivo | Métrica |
  |-----|----------|---------|
  | Disponibilidad | 99.9% | http_requests_total{status!~5..} / total |
  | Latencia p99 | < 500ms | histogram_quantile(0.99, http_duration_bucket) |
  | Tasa de error | < 0.1% | http_requests_total{status=~5..} / total |
  | Throughput | > 10K/s | rate(http_requests_total[5m]) |

Alertas (síntomas visibles para el usuario):
  - Tasa de error > 1% durante 5 min -> pagear al on-call
  - Latencia p99 > 1s durante 10 min -> pagear al on-call
  - Burn rate de SLO > 14x en 1h -> pagear al on-call
  - Throughput < 5K/s durante 5 min -> ticket (no pagear)

Lecciones:
  - OpenTelemetry unifica traces, métricas y logs
  - traceId es la forma de correlacionar todo
  - Logs JSON estructurados > texto plano
  - Alerta sobre SLOs, no sobre métricas de infraestructura
  - El collector desacopla la app del backend de observabilidad

El collector se sitúa entre tu app y los backends, y eso es lo que convierte cambiar Jaeger por Tempo (o añadir un segundo backend) en un cambio de configuración en lugar de un redespliegue:

flowchart diagram: App[

La versión ejecutable de este setup (instrumentación + configuración del collector + laboratorio docker-compose) está en el repo companion — enlazada en Lectura adicional.

Lo que funciona

  • Usar IDs de correlación: pasa trace_id por cada llamada entre servicios, línea de log y respuesta de error
  • Loguear en los límites: entrada y salida de peticiones, jobs y transacciones. Los puntos de entrega son donde fallan las cosas
  • Evitar loguear datos sensibles: nada de contraseñas, tokens o PII. Filtra a nivel de processor, no por disciplina
  • Definir SLOs y presupuestos de error: define qué significa “bien” y mide contra eso; mira la guía de monitoreo y alertas
  • La fatiga de alertas es real: pagea solo por problemas urgentes y accionables; todo lo demás es un ticket
  • Instrumenta una vez, exporta a cualquier parte: estandariza en OpenTelemetry y deja que el collector enrute a los backends. Cambiar Jaeger por Tempo luego es un cambio de config, no una reescritura
  • Adopta en orden: empieza con logs estructurados, añade métricas RED después, luego tracing. Cada capa se apoya en los IDs de correlación de la anterior

Errores Comunes

  • Loguear todo a nivel INFO: la señal se ahoga en ruido y nadie confía en ERROR
  • Métricas sin labels (sin dimensiones para segmentar) o con labels sin límite (user_id) que disparan la cardinalidad
  • Alertar sobre uso de CPU en lugar de síntomas visibles para el usuario: que la máquina esté ocupada no significa que el usuario sufra
  • Guardar logs indefinidamente sin política de retención: el costo de almacenamiento crece linealmente y el valor decae rápido
  • Instrumentar después del primer incidente: añadir IDs de correlación a 15 servicios a posteriori cuesta semanas
  • Mantener dashboards que nadie abre: si un panel no se consultó en un trimestre, bórralo

Lectura adicional

Preguntas frecuentes

¿Cuál es la diferencia entre logs, métricas y traces?

Los logs son eventos discretos que responden "¿qué pasó?". Las métricas son números agregados que responden "¿cómo va la tendencia?". Los traces siguen una petición entre servicios y responden "¿dónde se fue el tiempo?". Necesitas los tres porque cada uno cubre los puntos ciegos de los otros.

¿Cuánto tiempo debería retener los logs?

Retén logs de error y auditoría entre 30 y 90 días; los de debug pueden conservarse una semana. Ajusta según requisitos de cumplimiento y costo, y usa sampling en servicios de alto volumen, porque guardar cada línea de DEBUG es pagar por almacenar ruido.

¿Sobre qué debería alertar?

Alerta sobre síntomas visibles para el usuario: tasa de error, latencia, disponibilidad, burn rate del SLO. Evita pagear por métricas de infraestructura como CPU o memoria salvo que correlacionen directamente con impacto al usuario.

¿Qué es el burn rate de un SLO?

El burn rate mide qué tan rápido consumes tu presupuesto de error. Con un SLO del 99.9% (43.2 min de error permitido al mes), un burn rate de 14x significa que gastas el presupuesto 14 veces más rápido de lo normal; lo agotas en unas 2–3 horas. Alertar por burn rate detecta problemas antes de que incumplan el SLO.

¿Debería instrumentar todo desde el primer día?

No. Empieza con traces a nivel de petición en los servicios del camino visible para el usuario, logs estructurados en todas partes (es casi gratis con las librerías modernas) y un SLO por journey crítico. Expande cuando un incidente real revele un punto ciego. Instrumentar todo por adelantado produce datos que nadie lee.

¿Debería muestrear logs, traces o ambos?

En servicios de alto volumen, ambos, pero de forma distinta. Muestrea los traces en la cola (guarda todos los errores más una fracción de los éxitos) para no perder las peticiones interesantes. Muestrea o descarta logs INFO/DEBUG repetitivos, pero nunca muestrees los de ERROR — son los datos más baratos y con más señal que tienes. Las métricas ya vienen agregadas, así que no necesitan sampling.

¿Cómo depuro una petición lenta de principio a fin?

Parte de la métrica: encuentra qué servicio y endpoint se volvieron lentos en el dashboard RED. Toma un trace lento de ese endpoint y mira la cascada de spans — el span más largo es tu sospechoso. Luego lleva el trace_id de ese span a los logs: las líneas ERROR o WARN dentro de ese trace normalmente nombran la causa (consulta lenta, timeout downstream, tormenta de reintentos). Métrica → trace → log: tres saltos, cada uno acotando la búsqueda.

¿Con qué stack de observabilidad debería empezar?

OpenTelemetry para la instrumentación, porque desacopla tu código del backend. Después, lo más barato que funcione: Prometheus + Grafana + Loki + Jaeger auto-hospedado si puedes operarlo, o un vendor (Grafana Cloud, Datadog, Honeycomb) si no puedes. No elijas primero el backend — elige primero la instrumentación y mantén el backend intercambiable.