Ejercicio 1 — Las métricas RED
- Las series tras 20 requests:
http_requests_total{method="POST",route="reservations",status="201"} 18
http_requests_total{method="POST",route="reservations",status="409"} 2
http_request_duration_seconds_bucket{method="POST",route="reservations",le="0.4"} 17
http_request_duration_seconds_bucket{method="POST",route="reservations",le="0.8"} 20- La explosión: path crudo × 50 requests con 50 UUIDs distintos → 50 series de
http_requests_total+ 350 del histograma (7 buckets × 50): el índice de Prometheus crece linealmente con el tráfico ÚNICO (y a las 2 semanas: OOM del Prometheus). El fix: el label con la plantilla del resolver_match — la cardinalidad es por RUTA, no por request.
- Los paneles del dash con la anotación del deploy (41): la regresión se ve con la línea vertical — el p95 subió EXACTAMENTE en el deploy 43 (el postmortem del 47 lo confirma en 30 s de mirada).
Ejercicio 2 — Las trazas
- La traza del checkout (spans con duración):
POST /api/v1/reservations/pay 340ms
├─ db.tx: begin + lock reserva 120ms
│ └─ UPDATE seats WHERE id IN 85ms
├─ outbox.insert 6ms
├─ gateway.charge [payment.intent_id=int_9f2c] 110ms ← el tramo remoto, la 32/54
└─ email.enqueue (on_commit) 3ms- El sampling: 50 requests → 5 trazas (10%). El detalle del error: con el sampler por defecto (head-based), el 90% de los errores NO deja traza — la cura: el sampler de cola (tail sampling en el collector: keep si
status=ERROR) o el flag dinámico del incidente (traceidratio=1.0con ventana corta, la 27). El proyecto: ratio 0.1 + keep-errors en el collector.
- El hilo: el trace_id de la 45 aparece como atributo del span raíz y la propagación del contexto Celery (el task re-setea el contexto OTel con el extractor del mensaje): la traza conecta endpoint→worker→SQL. La consulta del incidente: tid → log (el detalle) + traza (el timeline): las dos vistas del §2 de la 45, un id.
Ejercicio 3 — Los SLOs
- Las queries:
# p95 del checkout (5m)
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{route="checkout"}[5m])))
# disponibilidad (30d)
1 - (sum(rate(http_requests_total{status=~"5.."}[30d])) / sum(rate(http_requests_total[30d])))
# convergencia de saga: % de PENDING con edad > 15 min
sagas_in_unknown / clamp_min(sagas_total, 1)- El budget: 99.5% → 0.5% de ~1.3M requests/mes = 6,500 requests lentos/mes el gasto máximo. Burn 3.2: se quema el budget en 30/3.2 ≈ 9.4 días — sin mejora, el mes termina con el SLO roto y la feature freeze implícita (el budget agotado congela features y prioriza fiabilidad: el contrato de los SRE).
- La tabla real (post-mejoras del 37-39): lectura p95 290 ms (< 300), checkout p95 640 ms (< 800), disponibilidad 99.96%, saga converge 0.04%. El sistema CUMPLE — el SLO que apretaría el próximo trimestre: checkout < 600 ms (el margen actual es 20%).
Ejercicio 4 — Las alertas
- Las 3 reglas:
- alert: CheckoutLatencyBudgetBurn
expr: |
(histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{route="checkout"}[1h])))
> 0.8)
and
(histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{route="checkout"}[5m])))
> 0.8)
for: 10m
labels: {severity: page} # burn multi-ventana: 1h confirma, 5m evita el spike
- alert: OutboxBacklog
expr: outbox_pending > 1000
for: 15m
labels: {severity: page}
- alert: ExpirationJobHeartbeat
expr: time() - max(expiration_job_last_run_seconds) > 300
labels: {severity: page} # la ausencia (29): el inventario congelado- El anti-ruido demostrado: el spike de 2 min (k6 corto) → la ventana de 5m no confirma → sin page (el spike de 2 min con budget intacto NO despierta a nadie); el fallo sostenido de 12 min → ambas ventanas en rojo → page. La política: page = hay fuego real y sostenido; el resto, ticket.
- El runbook de sagas_in_unknown (extracto): "Confirma: dash sagas + la query del 32 (
SELECT saga_id, intentos FROM purchase_saga WHERE estado='UNKNOWN_EXPIRED'). Causas: (1) pasarela caída → breaker del 54, esperar; (2) reconciliación agotada → runbook de la 32 (consultar panel de la pasarela, compensar/confirmar a mano con ledger); (3) bug del poller → rollback del 41. El on-call nuevo llegó a la causa en 6 min con el runbook (el test real: otro lo leyó)".
Ejercicio 5 — El game day
- La cronología (Redis apagado a T=0):
T+0s Redis muerto (caché + broker del 29)
T+8s p95 del browse sube (caché frío: 38 ms → 412 ms)
T+45s checkout p95 cruza 800 ms (la pasarela y la cola local no responden)
T+2m10s CheckoutLatencyBudgetBurn pagea ✓ (5m window confirma el burn)
T+4m30s ExpirationJobHeartbeat pagea ✓ (el worker sin broker murió de hambre)
T+6m DLQ/task_failed en ticket ✓Dos pages correctos, cronología limpia: la cadena funciona.
- La brecha: el
outbox_pendingNO disparó (el poller está vivo pero sin broker: los eventos se acumulan sin error visible en el app — el gauge sube pero la alerta era solo de DLQ). La alerta añadida:outbox_pending > 1000 for 15m(la del §4: la lista completa del juego). El p95 del browse con caché frío: cubierto por el burn del checkout (comparte causa), sin alerta propia — decisión documentada: una causa, una alerta.
- El test del on-call: el tercero con dash+runbook diagnosticó en 11 min: "Redis muerto → el worker de hambre → reiniciar Redis, drenar cola, verificar heartbeat". El criterio del game day: <15 min y SIN contactarte a ti — el sistema se opera solo, que es la única definición de observabilidad que importa.
Resumen del profesor
- RED con labels de plantilla (cardinalidad controlada) + métricas de negocio (sagas, outbox, DLQ): el dash de dos números (error rate, p95) responde "¿está sano?".
- Trazas OTel con sampling 10% + keep-errors: el timeline del request con el mismo tid del log.
- SLO con budget y burn multi-ventana: page solo el síntoma sostenido; el heartbeat (la ausencia) es la alerta que ningún error dispara — y el game day la prueba.