Stack: Prometheus + Grafana + OpenTelemetry · Proyecto: TicketFlow Estado: Publicada — SLOs y alertas por SLO, no por pánico Prerrequisito: Lección 45 — Logs estructurados
Objetivos
- Instrumentar TicketFlow con las métricas RED (rate, errors, duration) por endpoint y las de negocio (reservas/min, sagas en UNKNOWN).
- Añadir trazas distribuidas (OpenTelemetry) que conecten endpoint → servicio → BD → cola con el trace_id de la 45.
- 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:
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 responseEl 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:
# 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:
| SLI | SLO | Ventana |
|---|---|---|
| Latencia p95 de endpoints de lectura | < 300 ms | 30 d |
| Latencia p95 del checkout | < 800 ms | 30 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:
| Alerta | Umbral | Severidad |
|---|---|---|
| checkout p95 | burn 14.4×1h | page |
| http_5xx rate | > 1% × 5 min | page |
| sagas_in_unknown > 5 | 10 min | page (el dinero en limbo) |
| outbox_pending > 1000 | 15 min | page (los eventos no salen) |
| dlq_depth > 50 | 15 min | ticket (la 30) |
| task_failed_total rate | > 5/min × 10 min | ticket |
| 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
- RED por endpoint: ¿qué tres métricas y por qué el label de ruta jamás lleva el path crudo (cardinalidad)?
- ¿Qué añade la traza OTel al trace_id de la 45 y qué se muestrea (y por qué no 100%)?
- Define el SLO del checkout y su error budget: ¿qué significa burn rate 14.4 y por qué dispara page?
- Síntoma vs causa: ¿qué alertas pagean y qué van a ticket? ¿Por qué el CPU alto no pagea?
- ¿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.