A lo largo del módulo hemos ido posponiendo una decisión: qué es "ir bien". Tenemos métricas (06-01), trazas (06-02), patrones de resiliencia (06-03) y autoescalado (06-04), pero nadie ha dicho todavía cuántos errores en POST /v1/pedidos son aceptables, cuánto puede tardar la saga antes de que alguien deba mirar, ni a quién despierta el teléfono cuando la DLQ se llena a las tres de la mañana. Sin esas respuestas, la observabilidad es una colección de paneles bonitos que se consultan después del desastre. Esta lección la convierte en un sistema de operación: SLOs que traducen la experiencia del cliente a números, alertas que suenan solo cuando esos números peligran, guardias y runbooks que saben qué hacer, y una forma de gestionar incidentes y aprender de ellos sin buscar culpables.

Contenido

  1. SLI, SLO y SLA: definiciones y presupuesto de error
  2. Elegir los SLIs de TechCorp
  3. SLOs concretos y su presupuesto mensual
  4. Expresar los SLIs en PromQL con reglas de grabación
  5. Alertas basadas en SLO: burn rate multiventana
  6. Alertas por síntoma y por qué no alertar por causas
  7. Alertmanager: rutas por equipo, agrupación, silencios e inhibición
  8. Guardias y runbooks
  9. Gestión de incidentes: roles, severidades y comunicación
  10. Postmortem sin culpa: INC-2031
  11. Revisión de SLOs y política de presupuesto de error
  12. Tabla resumen de las alertas de TechCorp

  1. SLI, SLO y SLA: definiciones y presupuesto de error

Término Qué es Quién lo fija Ejemplo TechCorp
SLI (Service Level Indicator) Una medida de la calidad del servicio, normalmente una proporción de eventos buenos sobre el total Ingeniería Proporción de POST /v1/pedidos que no devuelven 5xx
SLO (Service Level Objective) Un objetivo interno sobre un SLI en una ventana Ingeniería + producto 99,9 % de los POST /v1/pedidos sin 5xx en 30 días
SLA (Service Level Agreement) Un contrato con consecuencias (penalizaciones) si se incumple Negocio/legal "Disponibilidad de la API del 99,5 % mensual" con los marketplaces que integran TechCorp

Reglas: el SLA es siempre más laxo que el SLO (si el SLO es 99,9 %, el SLA 99,5 %: el margen es lo que evita pagar penalizaciones); un SLI se expresa como proporción (0-1) para poder sumar, comparar y calcular presupuestos; y un SLO del 100 % no existe: cada nueve adicional multiplica el coste y ninguna dependencia (la nube, el PSP) lo ofrece.

El presupuesto de error es la parte del SLO que se permite fallar: 1 − SLO. Con un SLO del 99,9 % en 30 días, el 0,1 % de las peticiones pueden fallar; expresado en tiempo, si el servicio estuviera totalmente caído, 43 minutos al mes. Es la idea más útil de la lección: convierte "fiabilidad" en una moneda que se gasta en despliegues, experimentos y accidentes, y que cuando se agota obliga a parar (apartado 11).

  1. Elegir los SLIs de TechCorp

Un buen SLI mide lo que el cliente percibe, no lo que a ingeniería le resulta cómodo. Ana Ruiz no sabe qué es la CPU; sabe si su pedido se ha creado, si la página de productos carga rápido y si le llega la confirmación. Los cuatro SLIs de TechCorp, uno por pregunta:

SLI Pregunta del cliente Definición (bueno / total) Métrica de origen (06-01) Equipo dueño
Disponibilidad de pedidos "¿Puedo comprar?" POST /v1/pedidos con código ≠ 5xx / todos los POST /v1/pedidos http_requests_total medida en el gateway Pedidos
Latencia de catálogo "¿Carga rápido la tienda?" GET /v1/productos en < 300 ms / todos http_request_duration_seconds_bucket{le="0.3"} Experiencia de compra
Saga a tiempo "¿Me confirman el pedido enseguida?" Pedidos confirmados en < 60 s desde su creación / pedidos que llegan a resolverse saga_duracion_segundos_bucket{le="60"} Pedidos (y Pagos, Inventario)
Frescura del outbox (interno: predice el anterior) Instantes en que outbox_pendientes < 100 / instantes medidos outbox_pendientes Pedidos

Notas de diseño:

  • El SLI de disponibilidad se mide en el gateway (lo que el cliente ve), no en Pedidos: si Pedidos está sano pero el gateway no enruta, el cliente sufre igual. Los 4xx no cuentan como fallo: un 422 por datos inválidos es culpa del cliente; un 503 por Catálogo caído (06-03) cuenta, aunque sea "de otro".
  • El de latencia se define como proporción de peticiones bajo un umbral, no como "el p95 < 300 ms". La proporción es un SLI limpio (bueno/total) y aprovecha directamente el bucket le="0.3" del histograma; el p95 se sigue mirando en Grafana.
  • El de la saga necesita el histograma de 06-01 y excluye del denominador los pedidos cancelados por SIN_STOCK o PAGO_RECHAZADO (resolverse rápido con un rechazo también es "a tiempo": se cuenta saga_duracion_segundos al cerrar el pedido en cualquier estado final).
  • El del outbox es un SLI interno: no lo ve el cliente, pero cuando falla el de la saga fallará minutos después. Un buen sistema de SLOs tiene pocos SLIs de cara al cliente y algunos internos que avisan antes.

  1. SLOs concretos y su presupuesto mensual

SLO Objetivo Ventana Presupuesto de error En minutos de "caída total" al mes
Disponibilidad POST /v1/pedidos 99,9 % 30 días 0,1 % 43 min
Latencia GET /v1/productos < 300 ms 99,5 % 30 días 0,5 % 216 min
Saga confirmada < 60 s 99,5 % 30 días 0,5 % 216 min
Outbox fresco (< 100 pendientes) 99,9 % 30 días 0,1 % 43 min

Por qué esos números y no otros: 99,9 % en pedidos porque cada minuto sin vender cuesta dinero medible (Black Friday, 01-05) y porque la infraestructura de 05-02/05-04 (2 réplicas, rolling sin cortes) lo hace alcanzable; 99,5 % en latencia y saga porque un 0,5 % de páginas lentas o confirmaciones tardías es molesto pero no impide comprar, y porque dependen del PSP y de MongoDB en pico. La cuenta de minutos: 30 días × 24 h × 60 min × (1 − SLO); para 99,9 %: 43.200 × 0,001 = 43,2 min. Y una advertencia: los minutos son una intuición; el presupuesto real se mide en peticiones. Una caída del 100 % durante 43 minutos a las 4 de la mañana consume mucho menos presupuesto (pocas peticiones) que un 5 % de errores durante 8 horas en horario comercial.

  1. Expresar los SLIs en PromQL con reglas de grabación

Calcular histogram_quantile o divisiones de rate sobre 30 días en cada consulta es caro. Las reglas de grabación (recording rules) precalculan cada SLI en una serie nueva, con un nombre convencional sli:<nombre>:ratio_rate<ventana>, y las alertas y los paneles consultan esa serie:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: slos-techcorp
  namespace: techcorp
  labels: { release: kube-prometheus-stack }
spec:
  groups:
    - name: sli-pedidos-disponibilidad
      interval: 30s
      rules:
        # proporción de errores 5xx en POST /v1/pedidos, en varias ventanas
        - record: sli:pedidos_errores:ratio_rate5m
          expr: |
            sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[5m]))
              / sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[5m]))
        - record: sli:pedidos_errores:ratio_rate30m
          expr: |
            sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[30m]))
              / sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[30m]))
        - record: sli:pedidos_errores:ratio_rate1h
          expr: |
            sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[1h]))
              / sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[1h]))
        - record: sli:pedidos_errores:ratio_rate6h
          expr: |
            sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[6h]))
              / sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[6h]))
        - record: sli:pedidos_errores:ratio_rate30d
          expr: |
            sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[30d]))
              / sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[30d]))
    - name: sli-catalogo-latencia
      rules:
        - record: sli:catalogo_lentas:ratio_rate5m
          expr: |
            1 - (
              sum(rate(http_request_duration_seconds_bucket{servicio="servicio-catalogo", ruta="/v1/productos", le="0.3"}[5m]))
                / sum(rate(http_request_duration_seconds_count{servicio="servicio-catalogo", ruta="/v1/productos"}[5m]))
            )
    - name: sli-saga
      rules:
        - record: sli:saga_tardias:ratio_rate1h
          expr: |
            1 - (sum(rate(saga_duracion_segundos_bucket{le="60"}[1h])) / sum(rate(saga_duracion_segundos_count[1h])))
    - name: presupuesto-error
      rules:
        - record: slo:pedidos_presupuesto_restante:ratio
          expr: 1 - (sli:pedidos_errores:ratio_rate30d / 0.001)      # 1 = intacto, 0 = agotado, negativo = excedido
  • Cada regla record guarda el resultado de expr como una serie nueva cada interval. Las alertas del apartado siguiente combinan las ventanas de 5 m, 30 m, 1 h y 6 h; el panel de "presupuesto restante" usa la de 30 d.
  • Se registran las proporciones de fallo (errores, lentas, tardías) en lugar de las de éxito: los burn rates se calculan sobre el fallo.
  • El de latencia usa 1 − (buenas/total): bucket{le="0.3"} cuenta las que tardaron ≤ 300 ms.
  • slo:pedidos_presupuesto_restante:ratio es la métrica que se enseña a negocio: "nos queda el 62 % del presupuesto de este mes".

Las 30 días de ventana requieren retención suficiente en Prometheus (o Thanos/Mimir para el largo plazo, mención). Los cuatro SLIs se documentan también como texto en techcorp/plataforma/observabilidad/slos.md: definición, dueño, objetivo y fecha de revisión.

  1. Alertas basadas en SLO: burn rate multiventana

La alerta ingenua "tasa de errores > 1 % durante 5 minutos" tiene dos problemas: dispara con un pico de dos minutos que no gasta apenas presupuesto, y no dispara con un 0,5 % sostenido tres días que sí lo agota. La alternativa del SRE book es alertar por velocidad de consumo del presupuesto (burn rate): 1× significa que al ritmo actual el presupuesto se agota exactamente al final de la ventana de 30 días; 14,4× significa que se agotaría en 2 días (30/14,4). Se combinan dos ventanas (una larga para asegurar que es real, una corta para que la alerta se apague pronto al resolverse):

Alerta Ventana larga Ventana corta Burn rate Presupuesto consumido para disparar Severidad
Rápida 1 h 5 m 14,4× 2 % en 1 h critical (página)
Lenta 6 h 30 m 5 % en 6 h warning (ticket)

Para el SLO de 99,9 % (presupuesto 0,001), 14,4× equivale a una tasa de errores del 1,44 % y 6× al 0,6 %:

    - name: alertas-slo-pedidos
      rules:
        - alert: PedidosErrorBudgetBurnRapido
          expr: |
            sli:pedidos_errores:ratio_rate1h > (14.4 * 0.001)
              and
            sli:pedidos_errores:ratio_rate5m > (14.4 * 0.001)
          for: 2m
          labels:
            severity: critical
            equipo: pedidos
            slo: pedidos-disponibilidad
          annotations:
            resumen: "POST /v1/pedidos consume el presupuesto de error 14,4x más rápido de lo previsto"
            descripcion: "Tasa de 5xx {{ $value | humanizePercentage }} en la última hora (SLO 99,9 %). Ver runbook."
            runbook: "https://runbooks.techcorp.internal/pedidos/error-budget-burn"
            dashboard: "https://grafana.techcorp.internal/d/red-servicio?var-servicio=servicio-pedidos"
        - alert: PedidosErrorBudgetBurnLento
          expr: |
            sli:pedidos_errores:ratio_rate6h > (6 * 0.001)
              and
            sli:pedidos_errores:ratio_rate30m > (6 * 0.001)
          for: 15m
          labels: { severity: warning, equipo: pedidos, slo: pedidos-disponibilidad }
          annotations:
            resumen: "POST /v1/pedidos consume el presupuesto de error 6x más rápido de lo previsto"
            runbook: "https://runbooks.techcorp.internal/pedidos/error-budget-burn"
  • expr con and: las dos ventanas deben superar el umbral a la vez. La de 1 h evita que un pico de segundos despierte a nadie; la de 5 m hace que la alerta se resuelva minutos después de arreglar la causa (sin ella seguiría activa hasta que la hora entera se limpiara).
  • for: 2m: la condición debe mantenerse dos minutos antes de disparar; contra el ruido de un solo scrape.
  • labels: severity decide el canal (apartado 7); equipo decide el destinatario; slo agrupa.
  • annotations: lo que lee la persona de guardia a las tres de la mañana. Siempre runbook y dashboard; una alerta sin runbook no se despliega (regla de Plataforma, comprobada por un linter en CI).

Las mismas dos alertas se replican para la latencia de catálogo (CatalogoLatenciaBurnRapido, presupuesto 0,005: umbrales 7,2 % y 3 % de lentas) y la saga (SagaTardiaBurnRapido, sobre sli:saga_tardias). Con cuatro SLOs y dos alertas cada uno son ocho alertas de SLO en total: pocas, significativas y todas ligadas a algo que el cliente nota.

  1. Alertas por síntoma y por qué no alertar por causas

Las alertas de SLO cubren "el cliente sufre". Hay un segundo grupo de alertas por síntoma que anticipan que sufrirá o que algo está roto aunque el cliente aún no lo note; deben ser pocas y con umbral claro:

    - name: alertas-sintomas
      rules:
        - alert: OutboxAtascado
          expr: outbox_pendientes{servicio="servicio-pedidos"} > 100
          for: 5m
          labels: { severity: critical, equipo: pedidos }
          annotations: { resumen: "El outbox de Pedidos acumula {{ $value }} eventos sin publicar", runbook: "https://runbooks.techcorp.internal/pedidos/outbox-atascado" }
        - alert: DlqConMensajes
          expr: rabbitmq_queue_messages_ready{queue=~".+\\.dlq"} > 0
          for: 10m
          labels: { severity: warning, equipo: "{{ if eq $labels.queue \"pagos.stock.dlq\" }}pagos{{ else }}pedidos{{ end }}" }
          annotations: { resumen: "La cola {{ $labels.queue }} tiene {{ $value }} mensajes", runbook: "https://runbooks.techcorp.internal/comun/dlq" }
        - alert: PodEnCrashLoop
          expr: max_over_time(kube_pod_container_status_waiting_reason{namespace="techcorp", reason="CrashLoopBackOff"}[5m]) > 0
          for: 5m
          labels: { severity: warning, equipo: plataforma }
          annotations: { resumen: "{{ $labels.pod }} en CrashLoopBackOff", runbook: "https://runbooks.techcorp.internal/plataforma/crashloop" }
        - alert: CircuitoAbierto
          expr: circuit_breaker_estado == 2
          for: 5m
          labels: { severity: warning, equipo: pedidos }
          annotations: { resumen: "Circuito abierto hacia {{ $labels.dependencia }} desde {{ $labels.servicio }}", runbook: "https://runbooks.techcorp.internal/comun/circuito-abierto" }

(La equipo con plantilla en DlqConMensajes es una simplificación; en la práctica se hace una regla por cola con su equipo dueño de 02-01.) Otras del mismo tipo: CertificadoCaduca (probe_ssl_earliest_cert_expiry de Blackbox Exporter, < 14 días, mención), SondaReadyFallando más de 10 minutos, RabbitMQNodoCaido.

Lo que no se alerta como página: las causas. "CPU > 80 %", "memoria > 90 %", "disco al 70 %" pueden significar un problema o un HPA haciendo su trabajo (06-04); si el cliente no sufre y ningún síntoma aparece, no hay nada que hacer a las tres de la mañana. Se dejan como paneles y, como mucho, como severity: info a un canal de Slack que se lee por la mañana. La disciplina es esta: página → el cliente sufre o va a sufrir en minutos; ticket → algo está mal y debe arreglarse esta semana; info → conviene saberlo. Cada alerta que despierta a alguien y no requiere acción es una alerta que hay que borrar o degradar.

  1. Alertmanager: rutas por equipo, agrupación, silencios e inhibición

Prometheus evalúa las reglas; Alertmanager decide a quién y cómo avisar. Configuración del equipo de Plataforma (secreto alertmanager-config, versionado con los valores del chart):

route:
  receiver: slack-plataforma                 # destino por defecto
  group_by: [alertname, equipo]              # una notificación por alerta y equipo, no por pod
  group_wait: 30s                            # esperar 30 s a que lleguen alertas relacionadas antes de la primera notificación
  group_interval: 5m
  repeat_interval: 4h                        # recordatorio si sigue activa
  routes:
    - matchers: [severity="critical"]
      receiver: pagerduty-guardia            # página a quien esté de guardia
      continue: true                         # y además sigue evaluando: también llega a Slack
    - matchers: [equipo="pedidos"]
      receiver: slack-pedidos
    - matchers: [equipo="pagos"]
      receiver: slack-pagos-comunicaciones
    - matchers: [equipo="experiencia"]
      receiver: slack-experiencia-compra
    - matchers: [equipo="plataforma"]
      receiver: slack-plataforma
receivers:
  - name: pagerduty-guardia
    pagerduty_configs:
      - routing_key_file: /etc/alertmanager/secrets/pagerduty
        severity: critical
  - name: slack-pedidos
    slack_configs:
      - api_url_file: /etc/alertmanager/secrets/slack
        channel: "#alertas-pedidos"
        title: '{{ .CommonLabels.alertname }} ({{ .Status }})'
        text: '{{ range .Alerts }}{{ .Annotations.resumen }} — <{{ .Annotations.runbook }}|runbook>{{ "\n" }}{{ end }}'
  # slack-pagos-comunicaciones, slack-experiencia-compra, slack-plataforma: análogos
inhibit_rules:
  - source_matchers: [alertname="RabbitMQNodoCaido"]
    target_matchers: [alertname=~"DlqConMensajes|OutboxAtascado"]
    equal: [namespace]                       # si RabbitMQ está caído, no avisar además de sus consecuencias
  • Routing por severity y equipo: critical va a PagerDuty (la guardia) y al Slack del equipo (continue: true); warning solo al Slack del equipo. Los cuatro canales corresponden a los cuatro equipos de 02-01.
  • Agrupación (group_by): si 12 pods de Catálogo entran en CrashLoopBackOff, llega una notificación con 12 alertas, no 12 mensajes.
  • Silencios: durante un mantenimiento planificado (migración de RabbitMQ un domingo) se crea un silencio en la interfaz de Alertmanager con matchers (equipo="pedidos", 2 h) y un comentario; las alertas se evalúan pero no se envían.
  • Inhibición: una alerta "raíz" (RabbitMQNodoCaido) suprime las derivadas (OutboxAtascado, DlqConMensajes) para que la guardia reciba un aviso, no diez.
  • Slack y PagerDuty son ficticios en el curso; el patrón sirve igual con Opsgenie, Teams o correo.

  1. Guardias y runbooks

TechCorp, con ~25 técnicos, monta una guardia semanal por rotación entre los desarrolladores de los cuatro equipos (no solo Plataforma: quien construye, opera), con una persona primaria y otra secundaria, horario de compensación acordado y una regla: la guardia solo recibe alertas critical con runbook. Todo lo demás espera al horario laboral.

Un runbook es la página que se abre desde la alerta y dice qué mirar y qué hacer, escrita para alguien que no conoce el servicio y está medio dormido. Plantilla y ejemplo para PedidosErrorBudgetBurnRapido:

# Runbook: PedidosErrorBudgetBurnRapido
Severidad: critical · Equipo: Pedidos · Última revisión: 2026-08-01 · Dueño: Luis

## Qué significa
POST /v1/pedidos devuelve 5xx a más del 1,44 % de las peticiones durante la última hora
y los últimos 5 min. Los clientes no pueden comprar. Cada minuto cuenta contra el SLO 99,9 %.

## Comprobar en 5 minutos
1. Grafana "RED por servicio" (servicio=gateway, ruta=/v1/pedidos): ¿qué códigos? 503 → dependencia; 502/504 → Pedidos no responde; 500 → bug.
2. Grafana "RED por servicio" (servicio=servicio-pedidos): ¿latencia alta? ¿pods vivos?  kubectl -n techcorp get pods -l app=servicio-pedidos
3. Loki: {app="servicio-pedidos", nivel="error"} | json | line_format "{{.err.codigo}} {{.mensaje}}"
   - DEPENDENCIA_NO_DISPONIBLE + dependencia=catalogo → ir a "Catálogo caído" (abajo)
   - errores de pg → ir a "Base de datos"
4. ¿Ha habido despliegue en la última hora?  argocd app history servicio-pedidos
   Sí → revertir primero, investigar después: argocd app rollback servicio-pedidos <id-anterior>
   Canary (05-04): kubectl -n techcorp annotate ingress pedidos-canary nginx.ingress.kubernetes.io/canary-weight=0
5. Jaeger: buscar servicio=servicio-pedidos, error=true, últimos 15 min: ¿en qué span falla?

## Remedios por causa
- Catálogo caído: comprobar pods de catalogo; el breaker (06-03) ya protege; si es MongoDB → escalar a Plataforma.
- Base de datos: kubectl -n techcorp get pods -l app=postgres; conexiones: SELECT count(*) FROM pg_stat_activity; si están al máximo, reiniciar el pod de pedidos con más conexiones ociosas.
- Pods en CrashLoop: kubectl -n techcorp logs -l app=servicio-pedidos --previous | head; suele ser config (04-03) → revisar el último cambio del ConfigMap.
- Mensajes en DLQ tras la recuperación: node scripts/reprocesarDlq.js --cola pedidos.saga --max 200 (06-03) desde un pod efímero.

## Escalar a
Luis (líder de Pedidos) tras 15 min sin causa; Plataforma si es infraestructura; Marta si SEV1 (> 30 min o Black Friday).

## Después
Registrar en el canal #incidentes; si duró > 15 min o afectó a clientes, abrir postmortem (plantilla).

Cada alerta de la tabla del apartado 12 tiene el suyo en techcorp/plataforma/runbooks/, versionados en Git y con fecha de última revisión: un runbook con comandos que ya no funcionan es peor que ninguno. La regla de oro es que quien resuelve un incidente sin runbook lo escribe al terminar.

  1. Gestión de incidentes: roles, severidades y comunicación

Un incidente es cualquier situación que degrada el servicio y requiere coordinación. Cuando salta una página, la persona de guardia hace tres cosas: reconoce la alerta (para que no siga escalando), abre un canal (#inc-2031-pagos-stock en Slack) y evalúa la severidad:

Severidad Criterio Ejemplo TechCorp Respuesta
SEV1 Los clientes no pueden comprar o hay pérdida/daño de datos Gateway caído; POST /v1/pedidos con > 50 % de 5xx; cobros duplicados Página inmediata, comandante asignado, comunicación a negocio cada 30 min, todos los equipos disponibles
SEV2 Degradación importante o parte del flujo roto, con workaround o impacto parcial Saga atascada (pedidos en STOCK_RESERVADO sin confirmar); catálogo lento con p95 > 1 s; notificaciones no salen Página, se resuelve en horario ampliado, comunicación al inicio y al cierre
SEV3 Impacto menor o interno; sin efecto visible para el cliente DLQ con 3 mensajes; un pod en CrashLoopBackOff con réplicas sanas; presupuesto de error al 20 % Ticket, se resuelve en horario laboral

Los roles en un SEV1/SEV2, para que no todo el mundo teclee a la vez:

  • Comandante del incidente (incident commander): coordina, decide y asigna; no depura personalmente. Suele ser la persona de guardia hasta que alguien con más contexto la releva de forma explícita ("asumo el mando").
  • Comunicador: mantiene informados a negocio y soporte (un mensaje cada 30 min en #estado-plataforma con: qué pasa, impacto, qué se está haciendo, próxima actualización) para que nadie interrumpa a quien resuelve.
  • Operadores/investigadores: uno o varios; siguen el runbook, ejecutan cambios y anotan cada acción con hora en el canal.

Principios: estabilizar antes que entender (revertir el despliegue, escalar réplicas, desactivar el canary, reprocesar después: la causa raíz se busca cuando el cliente ya no sufre); una cronología en tiempo real en el canal (la reconstruirá el postmortem); y comunicación con negocio en su lenguaje ("no se pueden crear pedidos desde las 10:14; estimamos recuperación en 20 min; los pedidos ya creados no se ven afectados"), nunca en el nuestro ("pagos.stock está atascada por un mensaje venenoso").

  1. Postmortem sin culpa: INC-2031

Todo SEV1 y SEV2 termina con un postmortem en la semana siguiente. "Sin culpa" (blameless) significa que se asume que las personas actuaron razonablemente con la información que tenían, y que la pregunta es qué del sistema (código, herramientas, procesos, alertas) permitió que ocurriera y que durara lo que duró. Si el postmortem busca a quién regañar, la próxima vez la información se esconde. Plantilla y ejemplo resumido:

# Postmortem INC-2031 — pagos.stock atascada 40 minutos por un mensaje venenoso
Fecha: 2026-07-22 · Severidad: SEV2 · Duración: 40 min (10:14–10:54) · Comandante: guardia (Elena, Pagos)
Autores: equipo de Pagos y comunicaciones · Estado: acciones en curso

## Resumen
Un evento stock.reservado con `lineas: null` (bug en Inventario tras el despliegue 2.3.0) hizo que el
consumidor de pagos.stock lanzara TypeError. Con prefetch(1) en Pagos y reintento inmediato con requeue,
el mensaje volvía a la cabeza de la cola en bucle y bloqueó los demás cobros. 61 pedidos quedaron en
STOCK_RESERVADO; el vigilante canceló 38 por TIMEOUT_PAGO antes de la resolución.

## Impacto
61 clientes sin confirmación durante hasta 40 min; 38 pedidos cancelados y notificados como "problema en el pago"
(23 recompraron). Presupuesto de error de la saga consumido: 31 % del mes.

## Cronología (UTC+2)
10:03 Despliegue servicio-inventario 2.3.0 (canary 10 % → 100 % a las 10:10).
10:14 Primer TypeError en pagos.stock (Loki). Sin alerta: no existía DlqConMensajes ni SagaTardiaBurn.
10:31 Soporte avisa en Slack: clientes sin confirmación. La guardia abre #inc-2031.
10:38 Panel de la saga: 40 pedidos en STOCK_RESERVADO; pagos.stock con 58 mensajes listos y ninguno consumido.
10:44 Se identifica el mensaje venenoso en Loki por eventoId; se purga manualmente de la cola.
10:46 Cola desatascada; los cobros pendientes se procesan en 2 min.
10:50 Se revierte inventario a 2.2.4 (argocd rollback).
10:54 Saga normal. Se cierra el incidente.

## Causas (5 porqués, resumido)
1. Inventario publicó un evento inválido → falta de validación de salida contra el contrato (03-06); el test de Pact
   no cubría lineas nulas.
2. Pagos reintentaba en bucle → nack con requeue inmediato y prefetch(1) (aún no aplicaba 06-03).
3. Nadie lo vio en 17 min → sin alerta de síntoma sobre colas ni de SLO sobre la saga; el panel existía, nadie lo mira.
4. Tardó 13 min más en localizarse → sin runbook para "cola atascada".

## Qué funcionó
El vigilante de TIMEOUT_PAGO limitó el daño (reservas liberadas, clientes avisados). Los logs por eventoId permitieron
localizar el mensaje en minutos una vez se supo dónde mirar.

## Acciones correctivas (tareas con dueño y fecha)
- [Pagos] Cola de reintento con TTL + DLQ a la primera para errores permanentes; prefetch 3.  → hecho en 06-03
- [Pedidos] Alertas DlqConMensajes y SagaTardiaBurnRapido/Lento.                              → esta lección
- [Inventario] Validar eventos de salida con el esquema del contrato antes de escribir en outbox; test de Pact.
- [Plataforma] Runbook "cola atascada / DLQ" y script reprocesarDlq.js.                       → hecho en 06-03
- [Todos] Añadir "¿qué alerta lo habría detectado?" como pregunta fija del postmortem.

Las acciones se convierten en tareas en el backlog con dueño y fecha, y se revisan en la siguiente reunión de operaciones; un postmortem cuyas acciones no se ejecutan es un documento inútil. Y se comparte con toda la empresa: los incidentes son la fuente de aprendizaje más cara que existe; desaprovecharla es doble pérdida.

  1. Revisión de SLOs y política de presupuesto de error

Los SLOs no son eternos. Cada trimestre, cada equipo revisa con producto: ¿el SLI mide lo que el cliente nota? ¿el objetivo es demasiado ambicioso (siempre en rojo, nadie lo cree) o demasiado laxo (siempre al 100 %, no dice nada)? ¿han cambiado las expectativas (un marketplace nuevo exige más)? Se ajustan objetivos, se retiran SLIs inútiles y se añaden los que faltan.

Y el presupuesto de error se usa con una política escrita, acordada entre Marta y los equipos:

Presupuesto restante en el mes Política
> 50 % Normal: despliegues, experimentos, game days (06-03)
20-50 % Precaución: se revisan los despliegues de riesgo con otra persona; canary más largo (05-04)
< 20 % Solo cambios de fiabilidad y correcciones; se congelan las funcionalidades hasta que el presupuesto se recupere (la ventana de 30 días avanza)
Agotado / negativo Congelación total salvo correcciones; el equipo dedica el sprint a fiabilidad; postmortem obligatorio de cada contribuidor

Es el equilibrio que faltaba en el debate de velocidad de 05-03: las métricas DORA empujan a desplegar más a menudo; el presupuesto de error dice hasta cuándo. Un equipo con presupuesto sobrante debería atreverse a más (más despliegues, más experimentos), no menos: no gastar el presupuesto también es un desperdicio.

  1. Tabla resumen de las alertas de TechCorp

Alerta Condición Severidad Equipo Runbook
PedidosErrorBudgetBurnRapido 5xx en POST /v1/pedidos > 1,44 % en 1 h y 5 m critical Pedidos pedidos/error-budget-burn
PedidosErrorBudgetBurnLento > 0,6 % en 6 h y 30 m warning Pedidos pedidos/error-budget-burn
CatalogoLatenciaBurnRapido / Lento GET /v1/productos > 300 ms en > 7,2 % (1 h/5 m) / > 3 % (6 h/30 m) critical / warning Experiencia de compra catalogo/latencia
SagaTardiaBurnRapido / Lento Sagas > 60 s en > 7,2 % / > 3 % critical / warning Pedidos pedidos/saga-atascada
OutboxAtascado outbox_pendientes > 100 durante 5 m critical Pedidos pedidos/outbox-atascado
DlqConMensajes Cualquier *.dlq con mensajes 10 m warning Dueño de la cola comun/dlq
CircuitoAbierto circuit_breaker_estado == 2 durante 5 m warning Dueño del servicio comun/circuito-abierto
PodEnCrashLoop CrashLoopBackOff en techcorp 5 m warning Plataforma plataforma/crashloop
RabbitMQNodoCaido Nodo del clúster de RabbitMQ no disponible critical Plataforma plataforma/rabbitmq
CertificadoCaduca Certificado TLS < 14 días warning Plataforma plataforma/certificados
PresupuestoErrorBajo slo:*_presupuesto_restante:ratio < 0.2 info Cada equipo comun/politica-presupuesto

Once alertas (más las variantes por cola). Si en tres meses alguna no ha requerido acción nunca, se degrada o se elimina; si un incidente no fue detectado por ninguna, se añade la que lo habría hecho.

Errores Comunes y Consejos

  • Cien alertas. Cada una parece razonable; juntas producen fatiga y la guardia las ignora todas. Empezar por las de SLO y cuatro síntomas; añadir solo desde postmortems.
  • Alertar por umbral fijo de errores ("> 1 % durante 5 m"). Ruido con picos, silencio con degradaciones lentas. Burn rate multiventana.
  • SLO del 99,99 % "porque suena bien". Son 4 minutos al mes: incompatible con rolling de dependencias, con el PSP y con el presupuesto de la empresa. Elegir lo que el negocio necesita y la arquitectura permite.
  • Medir el SLI donde conviene y no donde el cliente está. Pedidos al 100 % con el gateway caído es un SLO cumplido y una tienda cerrada.
  • Alertas sin runbook. Despertar a alguien para que empiece de cero. Runbook o no se despliega.
  • Postmortem con nombres y adjetivos ("X desplegó sin probar"). La próxima vez nadie contará lo que pasó. Sistema, no personas.
  • Acciones del postmortem sin dueño ni fecha. Se olvidan en una semana. Tareas en el backlog, revisadas.
  • La guardia solo en Plataforma. Los desarrolladores no ven las consecuencias de su código. Quien construye, opera (con formación y compensación).
  • Consejo: montar un panel único "estado de SLOs" con las cuatro proporciones y el presupuesto restante, y ponerlo en la pantalla del equipo; es la conversación con producto que faltaba.
  • Consejo: probar las alertas: amtool alert add PedidosErrorBudgetBurnRapido severity=critical equipo=pedidos envía una alerta de prueba y comprueba el enrutado sin esperar a un incidente real.

Ejercicios

Ejercicio 1: SLO y presupuesto para Notificaciones

Define un SLI y un SLO para servicio-notificaciones ("el cliente recibe el correo de confirmación"), indica de qué métrica saldría (puedes proponer una nueva coherente con 06-01), calcula el presupuesto mensual y justifica una severidad para su alerta.

Ejercicio 2: la alerta de la saga

Escribe la regla SagaTardiaBurnRapido completa (expresión con las reglas de grabación necesarias, for, labels, annotations) para el SLO del 99,5 %, y explica por qué su umbral es 7,2 % y no 1,44 %.

Ejercicio 3: clasificar y actuar

Son las 02:10. Salta OutboxAtascado (outbox_pendientes = 340) y, a la vez, DlqConMensajes en inventario.pedidos.dlq con 2 mensajes. POST /v1/pedidos responde 202 con normalidad. Clasifica la severidad, decide qué alerta atiendes primero y describe los cinco primeros minutos siguiendo el estilo del runbook.

Soluciones

Ejercicio 1

SLI: proporción de correos de confirmación entregados al proveedor en menos de 5 minutos desde pedido.confirmado, sobre el total de pedido.confirmado recibidos. Métrica: histograma notificacion_latencia_segundos{tipo="confirmacion"} observado en Notificaciones desde ocurrido_en del evento hasta la respuesta 2xx del proveedor, con buckets [1, 5, 30, 60, 300, 900]; SLI = rate(..._bucket{le="300"}) / rate(..._count). SLO: 99 % en 30 días (un correo tardío es molesto, no impide comprar; el proveedor externo no garantiza más). Presupuesto: 1 % ≈ 432 minutos de "caída total" al mes, o unas 900 confirmaciones tardías de 90.000. Alerta: NotificacionesTardiasBurnRapido con severity: warning (no critical): no despierta a nadie; los correos se reintentan desde la cola (06-03) y llegan cuando el proveedor vuelve. Si el retraso supera horas pasaría a SEV3/SEV2 por impacto en soporte, pero no a página.

Ejercicio 2

Reglas de grabación necesarias: sli:saga_tardias:ratio_rate1h (ya definida) y sli:saga_tardias:ratio_rate5m (misma expresión con [5m]).

- alert: SagaTardiaBurnRapido
  expr: |
    sli:saga_tardias:ratio_rate1h > (14.4 * 0.005)
      and
    sli:saga_tardias:ratio_rate5m > (14.4 * 0.005)
  for: 2m
  labels: { severity: critical, equipo: pedidos, slo: saga-a-tiempo }
  annotations:
    resumen: "Las sagas tardan más de 60 s en {{ $value | humanizePercentage }} de los pedidos"
    descripcion: "El presupuesto de error del SLO 'saga < 60 s' (99,5 %) se consume 14,4x más rápido de lo previsto. Suele ser Pagos, Inventario o RabbitMQ."
    runbook: "https://runbooks.techcorp.internal/pedidos/saga-atascada"
    dashboard: "https://grafana.techcorp.internal/d/saga-pedidos"

El umbral es burn rate × presupuesto: 14,4 × 0,005 = 0,072 (7,2 %). Con un SLO del 99,5 % el presupuesto es cinco veces mayor que con 99,9 %, y por tanto la tasa de fallo que lo agota "en dos días" también es cinco veces mayor. El burn rate es el mismo (14,4×), el porcentaje absoluto depende del SLO.

Ejercicio 3

Severidad: POST /v1/pedidos sigue aceptando pedidos, pero ninguno avanza (los eventos no salen del outbox): la saga está parada para todo el que compre ahora, y en 10 minutos el vigilante no actuará (los pedidos están en PENDIENTE, no en STOCK_RESERVADO), así que se acumularán pedidos sin confirmar. Es un SEV2 (flujo roto con impacto creciente; no SEV1 porque no hay pérdida de datos: el outbox conserva todo). Se atiende primero OutboxAtascado: es critical, es la causa más "aguas arriba" y la DLQ de Inventario con 2 mensajes es warning y puede esperar (además podría ser consecuencia: si RabbitMQ va mal, ambas alertas encajan). Cinco minutos: (1) reconocer en PagerDuty, abrir #inc-XXXX-outbox; (2) Grafana "Saga de pedidos": outbox_pendientes sube en línea recta desde las 01:55, pedidos_creados_total normal, rabbitmq_queue_messages_ready{queue="inventario.pedidos"} = 0 → el relay no publica; (3) kubectl -n techcorp get pods -l app=servicio-pedidos → 2 pods Running; Loki {app="servicio-pedidos"} | json | mensaje=~"relay.*|rabbit.*" → "canal cerrado por el broker" a las 01:55 y ningún "lote publicado" desde entonces: el relay perdió el canal y no reconectó (un bug de reconexión); (4) remedio inmediato: kubectl -n techcorp rollout restart deployment/servicio-pedidos (los pods nuevos abren canal, el relay vacía el outbox en un minuto; el rolling de 05-04 no corta el POST); comprobar que outbox_pendientes baja; (5) anotar la cronología, ver los 2 mensajes de inventario.pedidos.dlq (probablemente relacionados: eventos publicados a medias antes del corte; se reprocesan con reprocesarDlq.js una vez el relay va), mensaje al comunicador/#estado-plataforma y, por la mañana, postmortem con acción "el relay debe reconectar con backoff y /health/ready debe fallar si el canal está cerrado" (06-03).

Conclusión

Con esta lección TechCorp deja de "mirar paneles" para operar con objetivos. Cuatro SLIs dicen lo que el cliente nota (disponibilidad de POST /v1/pedidos, latencia de GET /v1/productos bajo 300 ms, sagas confirmadas en menos de 60 s, frescura del outbox), con SLOs del 99,9 %/99,5 % y su presupuesto de error en minutos y en peticiones; las reglas de grabación sli:*:ratio_rate<ventana> los precalculan; las alertas de burn rate multiventana (PedidosErrorBudgetBurnRapido a 14,4× en 1 h/5 m, …Lento a 6× en 6 h/30 m) y un puñado de alertas por síntoma (OutboxAtascado, DlqConMensajes, CircuitoAbierto, PodEnCrashLoop) llegan a través de Alertmanager al equipo dueño y, solo si son critical, a la guardia; cada una con runbook; los incidentes se gestionan con severidades SEV1-SEV3, comandante y comunicador, y terminan en un postmortem sin culpa cuyas acciones son tareas; y la política de presupuesto de error decide cuándo se despliega y cuándo se para. Con esto termina el módulo de monitoreo y mantenimiento: los servicios de TechCorp emiten logs estructurados y métricas RED y de negocio (06-01), trazas que unen la petición HTTP con la saga (06-02), sobreviven a los fallos con timeouts, reintentos, circuit breakers, colas de reintento y el vigilante de la saga (06-03), escalan solos y con pruebas de carga (06-04) y se operan contra SLOs con alertas, guardias y postmortems (06-05). El sistema ya es observable, resiliente, escalable y operable; lo que todavía no es, salvo por las menciones a JWT y a la redacción de datos, es seguro. El módulo 7 empieza por ahí: autenticación y autorización con JWT/OAuth2 y Keycloak (07-01), seguridad en la comunicación entre servicios (07-02), prácticas de seguridad en el código y los datos (07-03) y seguridad en contenedores y Kubernetes (07-04).

Curso de Microservicios

Módulo 1: Introducción a los Microservicios

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

Módulo 8: Casos de Estudio y Ejemplos Prácticos

© Copyright 2026. Todos los derechos reservados