Módulo 4: Alerting On Error Budget Burn Rate

3. Manos a la obra: el evaluador de *burn rate*

Descripción

Esta lección construye scripts/burn_rate_evaluator.py: la implementación completa de las tres severidades de la Tabla 5-8 —no solo Ticket, como el Módulo 2 alcanzó a hacer con datos diarios—, corrida de verdad sobre los dos escenarios fijos de este módulo: la "mala semana" (Módulo 2, lección 7) y el mes "normal" (Módulo 2, lección 4). Es el primero de los tres motores que este módulo construye, y el más simple: Python puro, sin Docker, sin Terraform, sin ninguna infraestructura detrás — el prototipo exacto de la decisión que Alertmanager (lección 4) y CloudWatch (lección 5) van a tomar después, con infraestructura real alrededor.

Conexión con el módulo

Este script reutiliza burn_rate_of() de scripts/error_budget_calculator.py (Módulo 2, lección 6) sin modificarla — ninguna matemática de SLI nueva, solo la decisión de alerta construida encima de un número que la calculadora ya sabe producir. La lección 4 toma la misma decisión —las mismas tres severidades, los mismos dos escenarios— y la reimplementa como una regla real de Prometheus/Alertmanager, evaluada contra métricas scrapeadas en vez de contra un dataset importado en Python.


Paso 1 — De dónde salen los números: dos ventanas por escenario, ya conocidas

Antes del código, los datos. Este evaluador no recalcula el SLI desde cero — toma dos burn rates ya calculables con burn_rate_of(), uno por cada ventana (corta y larga), para cada uno de los dos escenarios de este módulo.

"Mala semana" (Módulo 2, lección 7, dataset BAD_WEEK). La ventana larga es la semana completa (los 7 días agregados como una sola ventana); la ventana corta es el último día (día 7), ya en mejora respecto al pico de la semana, pero todavía sin volver a un ritmo saludable —exactamente como la lección 7 del Módulo 2 lo dejó leído—.

"Normal" (Módulo 2, lección 4, dataset TRAFFIC_30_DAYS). La ventana larga es el mes completo de 30 días; la ventana corta es un día sano cualquiera dentro de ese mes (el día 1, con cero errores).

EscenarioVentana cortaBurn rate cortoVentana largaBurn rate largo
Mala semanaDía 7 (BAD_WEEK[-1])17,54xSemana completa (BAD_WEEK)43,41x
NormalDía 1 (TRAFFIC_30_DAYS[0])0,00xMes completo (TRAFFIC_30_DAYS)0,90x

Ambos números de cada fila ya aparecieron, por separado, en el Módulo 2: el 17,54x y el 43,41x son literales de la lección 7; el 0,90x es el mismo 90,2% de presupuesto consumido de la lección 4, expresado como burn rate en vez de porcentaje (90,2% del presupuesto consumido sobre la ventana completa del SLO es, por definición, un burn rate promedio de 0,90x sobre esa misma ventana). Esta lección no inventa ningún número — solo los reorganiza en pares (corto, largo) y les aplica la decisión de la Tabla 5-8.


Paso 2 — El script completo

Crea scripts/burn_rate_evaluator.py en la raíz de andes-cargo-infra/:

# burn_rate_evaluator.py
# Multi-window, multi-burn-rate ALERT DECISION. Given the short-window and long-window burn
# rate already observed for a scenario, decides fire/no-fire, tier by tier -- the exact
# decision Alertmanager (Module 4, lesson 4) and CloudWatch (Module 4, lesson 5) apply against
# live metrics, prototyped here first in plain Python before either is built.
#
# Reuses burn_rate_of() from scripts/error_budget_calculator.py (Module 2) -- no new SLI math,
# only the alerting decision layered on top of numbers that calculator already knows how to
# produce. Deterministic: BAD_WEEK (Module 2, lesson 7) and TRAFFIC_30_DAYS (Module 2, lesson 4)
# are the exact fixed datasets already committed to this project -- no random, no datetime.now().

from error_budget_calculator import TRAFFIC_30_DAYS, burn_rate_of

# The "mala semana" (bad week) dataset, unchanged from Module 2, lesson 7.
BAD_WEEK = [
    (1, 420, 418),
    (2, 435, 410),
    (3, 410, 379),
    (4, 428, 400),
    (5, 440, 421),
    (6, 300, 292),
    (7, 285, 280),
]

# Google SRE Workbook, Table 5-8 (sre.google/workbook/alerting-on-slos/).
# "long_window"/"short_window" are the real production windows -- this evaluator's INPUT
# already carries the burn rate for each window (computed the same way M4.4's PromQL
# expressions and M4.5's CloudWatch alarm period will compute it live).
TIERS = [
    {"name": "Page (fast)", "threshold": 14.4, "long_window": "1h", "short_window": "5m"},
    {"name": "Page (slow)", "threshold": 6.0, "long_window": "6h", "short_window": "30m"},
    {"name": "Ticket", "threshold": 1.0, "long_window": "3d", "short_window": "6h"},
]


def evaluate(tier, short_burn_rate, long_burn_rate):
    """Fires only when BOTH windows are at or above the tier's threshold -- the same
    two-window confirmation from Module 2, lesson 6 (ticket_tier_fires), generalized to
    all three severities of the real Google SRE table."""
    return short_burn_rate >= tier["threshold"] and long_burn_rate >= tier["threshold"]


def evaluate_scenario(label, short_burn_rate, long_burn_rate):
    print(f"--- {label} (short={short_burn_rate:.2f}x, long={long_burn_rate:.2f}x) ---")
    for tier in TIERS:
        fires = evaluate(tier, short_burn_rate, long_burn_rate)
        marker = "DISPARA" if fires else "no dispara"
        print(
            f"  {tier['name']:<12} >= {tier['threshold']:>4}x "
            f"({tier['long_window']}/{tier['short_window']}): {marker}"
        )


if __name__ == "__main__":
    # "Mala semana" (Modulo 2, leccion 7): ventana larga = la semana completa (7 dias
    # agregados como una sola ventana); ventana corta = el ultimo dia (dia 7), ya en
    # mejora pero todavia elevado -- exactamente como la leccion 7 lo dejo leido.
    bad_week_long = burn_rate_of(BAD_WEEK)
    bad_week_short = burn_rate_of([BAD_WEEK[-1]])
    evaluate_scenario("mala semana (M2.7)", bad_week_short, bad_week_long)

    print()

    # Escenario normal (Modulo 2, leccion 4): ventana larga = los 30 dias completos;
    # ventana corta = un dia sano cualquiera (dia 1, cero errores).
    normal_long = burn_rate_of(TRAFFIC_30_DAYS)
    normal_short = burn_rate_of([TRAFFIC_30_DAYS[0]])
    evaluate_scenario("normal (M2.4)", normal_short, normal_long)

TIERS es, literalmente, la Tabla 5-8 de la lección anterior, convertida en datos: cada severidad es un diccionario con su umbral y sus dos ventanas nominales (los nombres "1h", "5m" son solo etiquetas para el reporte — este script no mide tiempo real, recibe los burn rates ya calculados). evaluate() implementa exactamente la condición AND de la lección 2: ambas ventanas deben cruzar el umbral, nunca una sola. evaluate_scenario() corre las tres severidades sobre un mismo par de números y las imprime todas, para que quede claro, de un vistazo, cuáles disparan y cuáles no para un escenario completo.


Paso 3 — Corriendo el evaluador

python3 scripts/burn_rate_evaluator.py

Qué esperar (literal — corrido dos veces produce, línea por línea, el mismo resultado):

--- mala semana (M2.7) (short=17.54x, long=43.41x) ---
  Page (fast)  >= 14.4x (1h/5m): DISPARA
  Page (slow)  >=  6.0x (6h/30m): DISPARA
  Ticket       >=  1.0x (3d/6h): DISPARA

--- normal (M2.4) (short=0.00x, long=0.90x) ---
  Page (fast)  >= 14.4x (1h/5m): no dispara
  Page (slow)  >=  6.0x (6h/30m): no dispara
  Ticket       >=  1.0x (3d/6h): no dispara

Un contraste limpio: la "mala semana" dispara las tres severidades — no solo Ticket, como en el Módulo 2. El escenario "normal" no dispara ninguna. Este es exactamente el resultado que la lección 8 (el proyecto de este módulo) va a reproducir con los otros dos motores, Alertmanager y CloudWatch.


Leyendo el resultado: por qué "mala semana" cruza incluso el umbral más urgente

El burn rate corto de "mala semana" (17,54x, el día 7) sorprende a primera vista: el Módulo 2, lección 7 ya describió el día 7 como "el mejor día de la semana", con la tendencia claramente hacia la mejora. Y sin embargo, 17,54x sigue estando por encima del umbral más urgente de toda la Tabla 5-8 (14,4x, la fila Page (fast)). Esto no es un error del script — es la misma lectura honesta que el Módulo 2, lección 7 ya adelantó en su Ejercicio 2: "esta semana [...] nunca vuelve a bajar del umbral más urgente de Google en ningún día completo que el dataset cubra". Una mejora real (de 75,61x el día 3 a 17,54x el día 7) puede seguir estando, en términos absolutos, muy por encima de cualquier umbral de la tabla — "mejorando" y "ya saludable" son dos afirmaciones distintas, y solo la segunda apaga una alerta.

El escenario "normal", en cambio, ni siquiera se acerca al umbral menos exigente: 0,90x en la ventana larga —recordando que un burn rate de 1x es, por definición, exactamente el ritmo que el SLO permite sostener durante toda su ventana— está por debajo de lo que el propio SLO tolera como consumo sostenido. Es la confirmación numérica de algo que SLO.md ya declaró en su sección "Consequences": un mes con presupuesto ajustado (90,2% consumido) puede, al mismo tiempo, no representar ningún burn rate alarmante — ambas lecturas son ciertas a la vez, sin contradecirse, porque miden preguntas distintas.


Errores comunes

Confundir "el día 7 mejoró" con "el día 7 ya no debería disparar ninguna alerta" (repetido del Módulo 2, lección 7, ahora con consecuencias directas sobre una decisión de alerta real). Qué pasa: alguien, al ver short=17.54x para "mala semana", asume que hay un error en el script porque "la semana ya venía mejorando". Cómo detectarlo: si tu expectativa es que el escenario "mala semana" debería dejar de disparar antes del séptimo día. Cómo corregirlo: 17,54x sigue estando muy por encima de cualquier umbral de la Tabla 5-8 — una tendencia de mejora no es lo mismo que un valor ya saludable (cercano o por debajo de 1x). El script está haciendo exactamente lo que debe: seguir alertando mientras el consumo real, medido en la ventana corta, siga por encima del umbral, sin importar hacia dónde vaya la tendencia.

Modificar TIERS para "ajustar" el umbral de Ticket a un número distinto de 1,0x sin ninguna fuente (de romper la trazabilidad con la Tabla 5-8). Qué pasa: alguien, viendo que "mala semana" dispara las tres severidades de todos modos, decide que el umbral de Ticket es redundante y lo sube a, por ejemplo, 2x, "para reducir el ruido". Cómo detectarlo: si algún valor de TIERS en tu copia del script no coincide, dígito por dígito, con la Tabla 5-8 de la lección 2. Cómo corregirlo: los tres umbrales (14,4x, 6x, 1x) no son arbitrarios — cada uno está calibrado para que, sostenido exactamente en su ventana larga, consuma un porcentaje específico del presupuesto (2%, 5%, 10%, según el Ejercicio 3 de la lección 2). Cambiar un umbral sin recalcular esa relación rompe la garantía que le da sentido al patrón completo.

Tratar evaluate_scenario() como si necesitara error_budget_calculator.py y burn_rate_evaluator.py corriendo en procesos separados (de malentender el import). Qué pasa: alguien intenta ejecutar burn_rate_evaluator.py sin que error_budget_calculator.py esté en el mismo directorio, y el script falla con ModuleNotFoundError. Cómo detectarlo: el error exacto de Python al correr el script desde un directorio distinto. Cómo corregirlo: la línea from error_budget_calculator import TRAFFIC_30_DAYS, burn_rate_of requiere que ambos archivos vivan en scripts/, en la raíz de andes-cargo-infra/ — no son dos programas independientes, son un solo script que reutiliza funciones de otro, exactamente como el Módulo 2, lección 7 (bad_week_scenario.py) ya hizo con el mismo patrón de import.


Ejercicios

Ejercicio 1 — Calcula a mano si el escenario "normal" dispararía la fila Ticket (umbral 1,0x) si, hipotéticamente, su burn rate de ventana larga fuera 1,05x en vez de 0,90x, manteniendo el burn rate de ventana corta en 0,00x. Usa la definición de evaluate().

Ver solución

No dispararía. evaluate() exige que ambas condiciones se cumplan (short_burn_rate >= threshold and long_burn_rate >= threshold) — aunque la ventana larga hipotética (1,05x) sí cruzara el umbral de 1,0x, la ventana corta (0,00x) no lo hace, así que la condición completa (and) es falsa. Este ejercicio confirma, con un número distinto al de la lección, la misma lección del día 19 del Módulo 2: una sola ventana cruzando el umbral nunca es suficiente — ambas tienen que cruzarlo a la vez.

Ejercicio 2 — Modifica, en prosa (sin ejecutar nada), qué línea del script cambiarías para agregar una cuarta severidad hipotética, "Critical", con umbral 50x, ventana larga "15m" y ventana corta "2m". ¿Necesitarías cambiar evaluate() o evaluate_scenario()?

Ver solución

Bastaría con agregar un diccionario más a la lista TIERS: {"name": "Critical", "threshold": 50.0, "long_window": "15m", "short_window": "2m"}. Ni evaluate() ni evaluate_scenario() necesitarían ningún cambio — ambas funciones ya iteran sobre TIERS de forma genérica, sin ningún valor de severidad escrito directamente en su lógica. Esto confirma el mismo principio de diseño que el Módulo 2, lección 4 ya estableció con los parámetros por defecto de budget_report(): separar los datos (la tabla de severidades) de la lógica (cómo se evalúa cada fila) permite extender el comportamiento sin tocar ninguna función ya escrita.

Ejercicio 3 — Explica por qué este script recibe los burn rates de cada ventana ya calculados, en vez de recibir los datasets crudos (BAD_WEEK, TRAFFIC_30_DAYS) y calcular las ventanas internamente. ¿Qué ventaja tiene esta separación de cara a la lección 4 de este módulo?

Ver solución

Separar "calcular el burn rate de una ventana" (que ya hace burn_rate_of(), del Módulo 2) de "decidir si dispara una alerta dado un par de burn rates" (lo que hace evaluate(), nuevo en esta lección) es exactamente la misma separación que Prometheus y Alertmanager usan en producción real: Prometheus calcula tasas y valores agregados con PromQL (equivalente a burn_rate_of()), y una regla de alerta separada decide fire/no-fire sobre esos valores ya calculados (equivalente a evaluate()). Diseñar evaluate() para recibir números ya calculados, en vez de datasets crudos, es lo que hace posible que la lección 4 reimplemente exactamente la misma lógica de decisión como una expresión PromQL —que también opera sobre valores ya calculados, nunca sobre datos crudos directamente dentro de la regla de alerta—.


Resumen y siguiente paso

En esta lección construiste y corriste scripts/burn_rate_evaluator.py: la implementación completa de las tres severidades de la Tabla 5-8 de Google SRE, aplicada a los dos escenarios fijos de este módulo. El resultado: "mala semana" dispara las tres severidades (Page fast, Page slow, Ticket), con un burn rate de ventana corta (17,54x) que sigue por encima incluso del umbral más urgente pese a la mejora ya en curso; "normal" no dispara ninguna, con un burn rate de ventana larga (0,90x) por debajo de lo que el propio SLO tolera como consumo sostenido. Confirmaste que el script es determinista y que separa el cálculo del burn rate (ya construido en el Módulo 2) de la decisión de alerta (nueva en esta lección).

Antes de avanzar deberías poder: correr el script y obtener exactamente los mismos números de esta lección; explicar por qué "mala semana" dispara incluso la severidad más urgente pese a estar mejorando; y explicar la ventaja de recibir burn rates ya calculados en vez de datasets crudos.

La lección 4 toma exactamente esta misma decisión —las tres severidades, los mismos dos escenarios— y la reimplementa como una regla real de Prometheus/Alertmanager, evaluada contra métricas scrapeadas de un exportador real, enrutada a un receptor real.

Recursos

  1. Este mismo repositorio, Módulo 4, lección 2 (02-multiwindow-multiburn-rate-the-real-google-sre-pattern.md) — la Tabla 5-8 que TIERS implementa completa.
  2. Este mismo repositorio, Módulo 2, lecciones 4, 6 y 7 — burn_rate_of(), TRAFFIC_30_DAYS y BAD_WEEK, las tres piezas que este script reutiliza sin modificar.
  3. Google SRE Workbook — Alerting on SLOs — la fuente completa del patrón que este script implementa.