Módulo 10 · Observabilidad y depuración

Lección 46 — Métricas, trazas y alertas

Prometheus, Grafana, OpenTelemetry y alertas por SLO, no por pánico.

Publicada
En esta lección
  1. Objetivos
  2. 1. Las métricas RED y las de negocio
  3. 2. Las trazas: la pila completa de un request
  4. 3. El SLO: la promesa medible
  5. 4. Las alertas: despiertan por burn, no por pico
  6. 5. El runbook y el circuito completo
  7. Autoevaluación

Stack: Prometheus + Grafana + OpenTelemetry · Proyecto: TicketFlow Estado: Publicada — SLOs y alertas por SLO, no por pánico Prerrequisito: Lección 45 — Logs estructurados


Objetivos

  1. Instrumentar TicketFlow con las métricas RED (rate, errors, duration) por endpoint y las de negocio (reservas/min, sagas en UNKNOWN).
  2. Añadir trazas distribuidas (OpenTelemetry) que conecten endpoint → servicio → BD → cola con el trace_id de la 45.
  3. Definir SLOs y alertas que despiertan al on-call por lo que importa: el burn rate del error budget, no cada pico de CPU.

1. Las métricas RED y las de negocio

RED por endpoint (lo que el usuario experimenta): Rate (req/s), Errors (5xx y 4xx-de-dominio separados: la 26 decidió que el 409 es negocio, no error), Duration (histograma de latencia: p50/p95/p99). En Django: django-prometheus expone los contadores por ruta, o middleware propio:

python
REQUESTS = Counter("http_requests_total", "HTTP requests", ["method", "route", "status"])
LATENCY = Histogram("http_request_duration_seconds", "Latency", ["method", "route"],
                    buckets=(0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2))

class MetricsMiddleware:
    def __call__(self, request):
        t0 = time.monotonic()
        response = self.get_response(request)
        route = request.resolver_match.route if request.resolver_match else "unmatched"
        REQUESTS.labels(request.method, route, str(response.status_code)).inc()
        LATENCY.labels(request.method, route).observe(time.monotonic() - t0)
        return response

El matiz del label route (la plantilla /api/v1/reservations/{ref}, no el path completo): el path crudo genera una serie por UUID (la explosión de cardinalidad que tumba Prometheus). Y las métricas de NEGOCIO — las que el dashboard del CEO mira: reservations_created_total, sagas_in_unknown (gauge: la 32), outbox_pending (gauge: el poller retrasado), dlq_depth (la 30), seat_conflicts_total (los 409: la contención es dato). El dash de TicketFlow: fila RED por endpoint + fila de negocio + fila de infra (conexiones del 39, cola del 29) — el "¿está sano el sistema?" se responde mirando DOS números: el error rate y el p95.

2. Las trazas: la pila completa de un request

La traza distribuida es el trace_id de la 45 con músculo: cada tramo (endpoint → servicio → SQL → pasarela → cola → worker) es un span con duración y contexto. OpenTelemetry con el auto-instrumentation de Django/psycopg/redis/celery:

python
# core/otel.py
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

resource = Resource.create({"service.name": "ticketflow-web", "deployment.environment": "prod"})
tracer = trace.get_tracer("ticketflow")

# el tramo manual donde importa (la pasarela, la 32):
with tracer.start_as_current_span("gateway.charge") as span:
    span.set_attribute("payment.intent_id", intent_id)
    out = gateway.cobrar(intent_id, amount)
    span.set_attribute("payment.status", out.status)

La traza del checkout del 37 se lee como el flamegraph con timeline: POST /reservations (total 340ms) → db.tx reserva (120) → UPDATE seats (85) → outbox.insert (6) → gateway.charge (110, remoto) → email.enqueue (3). El sampling: parentbased_traceidratio=0.1 (10% de las trazas: el volumen completo es costo puro; el 100% SOLO en el incidente — el flag dinámico del 47). Y la conexión con el log: el span anota el trace_id (la 45): la consulta del incidente pasa del log a la traza con el mismo id — dos vistas del mismo hilo.

3. El SLO: la promesa medible

El SLO convierte "que vaya bien" en un número con ventana: 99.5% de los requests en < 400 ms (p95) medido en 30 días y 99.9% de disponibilidad (5xx < 0.1%). El error budget es el 1−SLO: el 0.5% de requests lentos/mes que el equipo PUEDE gastar. El burn rate (la velocidad a la que se gasta): ratio actual / ratio permitido — burn 1 = gastando exactamente el presupuesto; burn 14 en 1 h = el presupuesto del mes se quema en ~2 días. La tabla de SLOs de TicketFlow:

SLISLOVentana
Latencia p95 de endpoints de lectura< 300 ms30 d
Latencia p95 del checkout< 800 ms30 d
Disponibilidad (no-5xx)99.9%30 d
Convergencia de la saga (PENDING > 15 min)< 0.1%30 d

El SLO del negocio que no es HTTP: la saga convergiendo (el "el usuario quedó confirmado en <15 min" es la promesa real del 32).

4. Las alertas: despiertan por burn, no por pico

La jerarquía de la madurez: la alerta de pico (CPU > 80%: el paging innecesario — el sistema puede estar SANO a 80%) se sustituye por la de burn rate (Google SRE, multi-ventana): alerta page (despierta a alguien) si burn > 14.4 en 1 h Y > 6 en 5 min (evita la falsa alarma del spike de 2 min); alerta ticket (el horario laboral) si burn > 6 en 6 h. Las alertas de síntoma (el usuario lo sufre: p95, error rate, sagas UNKNOWN) vs las de causa (CPU, disco): las de causa van a ticket/dash, las de síntoma a page. El catálogo mínimo de TicketFlow:

AlertaUmbralSeveridad
checkout p95burn 14.4×1hpage
http_5xx rate> 1% × 5 minpage
sagas_in_unknown > 510 minpage (el dinero en limbo)
outbox_pending > 100015 minpage (los eventos no salen)
dlq_depth > 5015 minticket (la 30)
task_failed_total rate> 5/min × 10 minticket
heartbeat expiración > 5 min—page (la 29: el job que no corre)

El heartbeat (la ausencia) es la alerta que solo el 46 puede dar: el job de expiración muerto NO dispara errores — el inventario se congela en silencio hasta que el cliente lo nota.

5. El runbook y el circuito completo

Cada alerta page lleva runbook: la URL del dash que confirma, el comando del diagnóstico (el trace de la 45/§2), las 3 causas más probables y su fix, y el enlace al rollback del 41. La alerta sin runbook es un pager sin instrucciones. El circuito completo del incidente: alerta page → runbook → dash (confirma el síntoma) → trace (localiza el tramo) → log (el detalle) → fix/rollback → postmortem (47) → la alerta nueva o ajustada. Y el test de la cadena: el game day (el ejercicio del 47): se apaga el Redis en staging → ¿dispara la page esperada en <5 min? ¿El runbook guió al on-call? La observabilidad no probada es teoría.


Autoevaluación

  1. RED por endpoint: ¿qué tres métricas y por qué el label de ruta jamás lleva el path crudo (cardinalidad)?
  2. ¿Qué añade la traza OTel al trace_id de la 45 y qué se muestrea (y por qué no 100%)?
  3. Define el SLO del checkout y su error budget: ¿qué significa burn rate 14.4 y por qué dispara page?
  4. Síntoma vs causa: ¿qué alertas pagean y qué van a ticket? ¿Por qué el CPU alto no pagea?
  5. ¿Por qué el heartbeat de la expiración es page y qué test valida toda la cadena (game day)?

Continúa con los ejercicios. Las solutions.md solo tras intentarlo.