ЁЯПл The SchoolтА║ЁЯй║ ObservabilityтА║ЁЯЪи рдзрдбрд╛ 08 тАФ Alerting: рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдВрдирд╛ рддреНрд░рд╛рд╕ рд╣реЛрдд рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛рдЪ рдХреБрдгрд╛рд▓рд╛ рдЙрдард╡рд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЪи рдзрдбрд╛ 08 тАФ Alerting: рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдВрдирд╛ рддреНрд░рд╛рд╕ рд╣реЛрдд рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛рдЪ рдХреБрдгрд╛рд▓рд╛ рдЙрдард╡рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 08 ┬╖ рдорд╛рдЧреАрд▓: lesson-07-prometheus-grafana ┬╖ рдкреБрдвреАрд▓: lesson-09-slos


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ07, рдЖрдгрд┐ рднрд╛рдЧ 2 рдЪрд╛ рд╢реЗрд╡рдЯ: alerting. рддреБрдореНрд╣реА symptoms рд╡рд░ (users рдирд╛ рдЬреЗ рдЬрд╛рдгрд╡рддреЗ) alert рдХрд░рд╛рдпрд▓рд╛ рд╢рд┐рдХрддрд╛, causes рд╡рд░ (CPU) рдирд╛рд╣реА; burn rate рдореНрд╣рдгрдЬреЗ рдХрд╛рдп, рдЖрдгрд┐ Google SRE Workbook рдЪреЗ multi-window, multi-burn-rate alerts. burn_rate() рдЖрдгрд┐ burn_alerts() obs/reliability.py рдордзреНрдпреЗ рдЖрд╣реЗрдд; obs/demo.py рдордзрд▓реЗ alerting() рдкреВрд░реНрдг 8 рддрд╛рд╕рд╛рдВрдЪрд╛ рджрд┐рд╡рд╕ рддреНрдпрд╛рдВрдЪреНрдпрд╛рдордзреВрди рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрдд рдПрдХ рдзреЛрдХреНрдпрд╛рдЪреА рдШрдВрдЯрд╛ ЁЯЪи рдЖрд╣реЗ рдЬреА рд░рд╛рддреНрд░реА рдХрддрд░рд┐рдирд╛рд▓рд╛ рдЙрдард╡рддреЗ. рдзрдбрд╛ 01 рдордзрд▓реА boiler рд╡рд░рдЪреА рдШрдВрдЯрд╛ рдЖрдард╡рддреЗ? рддреА 64 рдорд┐рдирд┐рдЯреЗ рд╡рд╛рдЬрд▓реА рдЖрдгрд┐ рдЖрдЬрд╛рд░реА рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рд╕рд╛рдареА рдПрдХрджрд╛рд╣реА рдирд╛рд╣реА. рдХрддрд░рд┐рдирд╛ рддрд┐рдЪреНрдпрд╛рдХрдбреЗ рджреБрд░реНрд▓рдХреНрд╖ рдХрд░реВ рд▓рд╛рдЧрд▓реА. рдШрдВрдЯреЗрдХрдбреВрди рд╣реЛрдК рд╢рдХрдгрд╛рд░реА рд╣реА рд╕рдЧрд│реНрдпрд╛рдд рд╡рд╛рдИрдЯ рдЧреЛрд╖реНрдЯ рдЖрд╣реЗ.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдирд╡реЗ рдирд┐рдпрдо рд▓рд┐рд╣рд┐рддреЗ. рдШрдВрдЯрд╛ boiler рдЪреЗ рдирд╡реНрд╣реЗ, рд╡рд┐рджреНрдпрд╛рд░реНрдереНрдпрд╛рдВрдЪреЗ рдРрдХрддреЗ:

рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдирд┐рдпрдо рджреЛрди рдШрдбреНрдпрд╛рд│реЗ рддрдкрд╛рд╕рддреЛ. рдореЛрдареЗ рдШрдбреНрдпрд╛рд│ (рд╢реЗрд╡рдЯрдЪрд╛ рдПрдХ рддрд╛рд╕) рд╡рд┐рдЪрд╛рд░рддреЗ: "рд╣реЗ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдард░рдгреНрдпрд╛рдЗрддрдХреЗ рдореЛрдареЗ рдЖрд╣реЗ рдХрд╛?" рдЫреЛрдЯреЗ рдШрдбреНрдпрд╛рд│ (рд╢реЗрд╡рдЯрдЪреА 5 рдорд┐рдирд┐рдЯреЗ) рд╡рд┐рдЪрд╛рд░рддреЗ: "рд╣реЗ рдЕрдЬреВрдирд╣реА рдШрдбрдд рдЖрд╣реЗ рдХрд╛?" рджреЛрдШрд╛рдВрдиреАрд╣реА рд╣реЛ рдореНрд╣рдЯрд▓реЗ рдкрд╛рд╣рд┐рдЬреЗ. рддреНрдпрд╛рдореБрд│реЗ рдПрдХрд╛ рдЫреЛрдЯреНрдпрд╛ рд╢рд┐рдВрдХреЗрдиреЗ рдШрдВрдЯрд╛ рд╡рд╛рдЬрдд рдирд╛рд╣реА, рдЖрдгрд┐ рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдмрд░реЗ рдЭрд╛рд▓реНрдпрд╛рд╡рд░ рд▓рдЧреЗрдЪрдЪ рдШрдВрдЯрд╛ рдерд╛рдВрдмрддреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart TB
    err["ЁЯУИ error ratio per minute<br/>SLO 99.9% тЖТ budget rate 0.1%"]
    br["burn rate = error ratio ├╖ (1 тИТ SLO)<br/>0.9% тЖТ 9├Ч ┬╖ 6% тЖТ 60├Ч"]
    err --> br
    br --> pg{"ЁЯЪи PAGE<br/>тЙе 14.4├Ч over 1 h<br/>AND over 5 min"}
    br --> tk{"ЁЯУЭ TICKET<br/>тЙе 6├Ч over 6 h<br/>AND over 30 min"}
    pg -->|"bad deploy"| p1["minutes 409тАУ434<br/>9 min after it began"]
    tk -->|"slow leak"| t1["minutes 266тАУ390<br/>nobody woken"]
    cpu["ЁЯФХ CPU > 80%: 64 minutes of noise"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/observability/lesson-diagrams.html#l08

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг page рдорд╣рд╛рдЧ рдЕрд╕рддреЗ: рддреЗ рдПрдХрд╛ рд╡реНрдпрдХреНрддреАрд▓рд╛ рдЙрдард╡рддреЗ, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрдЯреЗ page рддрд┐рд▓рд╛ рдкреБрдврдЪреНрдпрд╛рдХрдбреЗ рджреБрд░реНрд▓рдХреНрд╖ рдХрд░рд╛рдпрд▓рд╛ рд╢рд┐рдХрд╡рддреЗ. CPU рд╣реЗ cause рдЖрд╣реЗ: рддреЗ 64 рдорд┐рдирд┐рдЯреЗ 80% рдЪреНрдпрд╛ рд╡рд░ рд╣реЛрддреЗ рдЖрдгрд┐ рдкрд╛рд▓рдХрд╛рдВрдирд╛ рдХрд╛рд╣реАрдЪ рддреНрд░рд╛рд╕ рдирд╡реНрд╣рддрд╛. Burn-rate alerts рдкрд╛рд▓рдХрд╛рдВрдирд╛ рдЬреЗ рдЬрд╛рдгрд╡рддреЗ рддреНрдпрд╛рд╡рд░ рд╡рд╛рдЬрддрд╛рдд, budget рдХрд┐рддреА рдЬрд▓рдж рд╕рдВрдкрдд рдЖрд╣реЗ рддреНрдпрд╛ рдкреНрд░рдорд╛рдгрд╛рдд. рдореЛрдард╛ рдЬрд▓рдж burn рдХрд╛рд╣реА рдорд┐рдирд┐рдЯрд╛рдВрдд рдХреБрдгрд╛рд▓рд╛ рддрд░реА рдЙрдард╡рддреЛ. рдЫреЛрдЯрд╛ рд╣рд│реВ burn ticket рдмрдирддреЛ. рдЫреЛрдЯрд╛рд╕рд╛ blip рдХрд╛рд╣реАрдЪ рдХрд░рдд рдирд╛рд╣реА тАФ рдХрд╛рд░рдг рддреНрдпрд╛рдиреЗ рдЦрд░реЛрдЦрд░рдЪ рдлрд╛рд░рд╕рд╛ рдлрд░рдХ рдкрдбрд▓рд╛ рдирд╛рд╣реА.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

obs/reliability.py рдордзрд▓реЗ burn_rate(error_ratio, slo) budget rate рдиреЗ рднрд╛рдЧрд╛рдХрд╛рд░ рдХрд░рддреЗ. window_ratio(errors, requests, end, minutes) рдореНрд╣рдгрдЬреЗ end рдкреВрд░реНрд╡реАрдЪреНрдпрд╛ рд╢реЗрд╡рдЯрдЪреНрдпрд╛ minutes рдЪрд╛ error ratio. burn_alerts(errors, requests, slo=0.999) рдкреНрд░рддреНрдпреЗрдХ minute рддрдкрд╛рд╕рддреЗ: рдЬрд░ 1 h рдЖрдгрд┐ 5 min рдЪреЗ burn rates рджреЛрдиреНрд╣реА тЙе 14.4 рдЕрд╕рддреАрд▓ рддрд░ рддреЗ page рдХрд░рддреЗ; рдирд╛рд╣реАрддрд░ рдЬрд░ 6 h рдЖрдгрд┐ 30 min рдЪреЗ burn rates рджреЛрдиреНрд╣реА тЙе 6 рдЕрд╕рддреАрд▓ рддрд░ рддреЗ ticket рдЙрдШрдбрддреЗ. (else Alertmanager рдЪреНрдпрд╛ inhibit rule рдЪреА рднреВрдорд┐рдХрд╛ рдмрдЬрд╛рд╡рддреЗ: page рдЪрд╛рд▓реВ рдЕрд╕рддрд╛рдирд╛ ticket рдирд╛рд╣реА. рджрд┐рд╡рд╕ minute 0 рд▓рд╛ рд╕реБрд░реВ рд╣реЛрддреЛ, рдореНрд╣рдгреВрди рджрд┐рд╡рд╕рд╛рдЪреНрдпрд╛ рд╕реБрд░реБрд╡рд╛рддреАрд▓рд╛ 6 h window рдлрдХреНрдд рдЖрддрд╛рдкрд░реНрдпрдВрддрдЪреА рдорд┐рдирд┐рдЯреЗ рд╡реНрдпрд╛рдкрддреЗ.) alerting() рджрд┐рд╡рд╕ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддреЗ: рдПрдХ slow leak (0.9%, minutes 100тАУ379) рдЖрдгрд┐ рдПрдХ bad deploy (6%, minutes 400тАУ431). demo.py рдордзрд▓реЗ spans() minute ranges print рдХрд░рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

Bad deploy рдЫреЛрдЯрд╛ рдХрд░рд╛, рдордЧ рдПрдХрд╕рд╛рд░рдЦреА рд╕рдорд╕реНрдпрд╛ рджреЛрди рдЖрдХрд╛рд░рд╛рдВрдд рдХрд░реВрди рдкрд╛рд╣рд╛:

python3 obs/demo.py alerting
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from demo import ERR, REQ, spans; from reliability import burn_alerts, burn_rate
print(f"burn rate of 0.9% errors on a 99.9% SLO: {burn_rate(0.009, 0.999):.0f}├Ч ┬╖ of 6%: {burn_rate(0.06, 0.999):.0f}├Ч")
for bad_minutes in (3, 10, 32):
    err = [1] * 480
    for m in range(400, 400 + bad_minutes): err[m] = 60
    pages, tickets = burn_alerts(err, REQ)
    print(f"6% errors for {bad_minutes:>2} min тЖТ page: {spans(pages)} ┬╖ ticket: {spans(tickets)}")
for pct in (1.5, 3.0):
    err = [1] * 480
    for m in range(100, 480): err[m] = int(pct * 10)
    pages, tickets = burn_alerts(err, REQ)
    print(f"{pct}% errors from minute 100 тЖТ first page {pages[0] if pages else 'never'} ┬╖ first ticket {tickets[0] if tickets else 'never'}")
EOF
python3 obs/test_obs.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

alerting рд╣реЗ print рдХрд░рддреЗ:

тФАтФА 8 hours of traffic, SLO 99.9%: a slow leak (0.9% errors, minutes 100тАУ379) and a bad deploy (6%, minutes 400тАУ431)
   CPU > 80% alerts: 64 minutes of noise that do not track what users feel
   burn-rate TICKET (тЙе6├Ч over 6 h AND 30 min): minutes 266тАУ390, 401тАУ408, 435тАУ458 тАФ first the slow leak (nobody woken at night), then around the deploy while the page is not firing
   burn-rate PAGE (тЙе14.4├Ч over 1 h AND 5 min): minutes 409тАУ434 тАФ the bad deploy, 9 minutes after it began
   page for fast burns that hurt users now; open a ticket for slow burns; never page on a cause like CPU

рддреБрдордЪрд╛ snippet рд╣реЗ print рдХрд░рддреЛ:

burn rate of 0.9% errors on a 99.9% SLO: 9├Ч ┬╖ of 6%: 60├Ч
6% errors for  3 min тЖТ page: never ┬╖ ticket: never
6% errors for 10 min тЖТ page: never ┬╖ ticket: never
6% errors for 32 min тЖТ page: minutes 413тАУ434 ┬╖ ticket: minutes 435тАУ458
1.5% errors from minute 100 тЖТ first page 157 ┬╖ first ticket 155
3.0% errors from minute 100 тЖТ first page 127 ┬╖ first ticket 120

Tests рдордзреНрдпреЗ тЬЕ L08 the bad deploy pages within 10 minutes; the slow leak only opens a ticket рдЕрд╕рддреЛ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Slow leak (9├Ч) рдиреЗ рдХрдзреАрдЪ page рдХреЗрд▓реЗ рдирд╛рд╣реА тАФ рддреНрдпрд╛рдиреЗ minute 266 рд▓рд╛ ticket рдЙрдШрдбрд▓реЗ. Bad deploy рдиреЗ minute 409 рд▓рд╛ page рдХреЗрд▓реЗ, рдЖрдгрд┐ page 434 рд▓рд╛ рдерд╛рдВрдмрд▓реЗ, errors 431 рд▓рд╛ рд╕рдВрдкрд▓реНрдпрд╛рдирдВрддрд░ рддреАрди рдорд┐рдирд┐рдЯрд╛рдВрдиреА тАФ рд╣реЗ 5-minute window рдЪреЗ рдХрд╛рдо. 6% рдЪрд╛ 10-minute blip рдХрд╛рд╣реАрдЪ рдХрд░реВ рд╢рдХрд▓рд╛ рдирд╛рд╣реА: рддреНрдпрд╛рдиреЗ 600 ├╖ 43,200 тЙИ рдорд╣рд┐рдиреНрдпрд╛рдЪреНрдпрд╛ budget рдЪрд╛ 1.4% рдЦрд░реНрдЪ рдХреЗрд▓рд╛, page рд▓рд╛ рдпреЛрдЧреНрдп рдард░рд╡рдгрд╛рд▒реНрдпрд╛ 2% рдкреЗрдХреНрд╖рд╛ рдХрдореА. рдЖрдзреА leak рдирд╕рд▓реЗрд▓реНрдпрд╛ рддреНрдпрд╛рдЪ 32-minute deploy рдиреЗ 409 рдРрд╡рдЬреА 413 рд▓рд╛ page рдХреЗрд▓реЗ тАФ leak рдиреЗ рдЖрдзреАрдЪ рд╢реЗрд╡рдЯрдЪреНрдпрд╛ рддрд╛рд╕рд╛рдЪреНрдпрд╛ budget рдЪрд╛ рдХрд╛рд╣реА рднрд╛рдЧ рд╡рд╛рдкрд░рд▓рд╛ рд╣реЛрддрд╛. рдЖрдгрд┐ рдПрдХрд╕рд╛рд░рдЦреНрдпрд╛ 3% (30├Ч) рдиреЗ 27 рдорд┐рдирд┐рдЯрд╛рдВрдд page рдХреЗрд▓реЗ; рдПрдХрд╕рд╛рд░рдЦреНрдпрд╛ 1.5% (15├Ч) рд▓рд╛ 57 рдорд┐рдирд┐рдЯреЗ рд▓рд╛рдЧрд▓реА: burn рдЬрд┐рддрдХрд╛ рд▓рд╣рд╛рди, рддрд┐рддрдХрд╛ 1-hour window рд▓рд╛ рдЬрд╛рд╕реНрдд рд╡реЗрд│ рд▓рд╛рдЧрддреЛ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдкреНрд░рддреНрдпреЗрдХ window рд╕рд╛рдареА error ratio рдЪреЗ Prometheus recording rules, рдордЧ 99.9% SLO рд╕рд╛рдареА Workbook рдЪреЗ burn-rate alerts (0.001 рд╣рд╛ budget rate рдЖрд╣реЗ):

groups:
  - name: results-api-slo
    rules:
      - record: job:slo_errors_per_request:ratio_rate5m
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))
      - record: job:slo_errors_per_request:ratio_rate30m
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[30m])) / sum by (job) (rate(http_requests_total[30m]))
      - record: job:slo_errors_per_request:ratio_rate1h
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[1h])) / sum by (job) (rate(http_requests_total[1h]))
      - record: job:slo_errors_per_request:ratio_rate6h
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[6h])) / sum by (job) (rate(http_requests_total[6h]))

      - alert: ResultsApiErrorBudgetFastBurn
        expr: |
          job:slo_errors_per_request:ratio_rate1h{job="results-api"} > (14.4 * 0.001)
          and
          job:slo_errors_per_request:ratio_rate5m{job="results-api"} > (14.4 * 0.001)
        labels:
          severity: page
        annotations:
          summary: "results-api is burning its 30-day error budget 14.4├Ч too fast (2% per hour)"
          runbook_url: https://runbooks.example.school/results-api/error-budget-burn

      - alert: ResultsApiErrorBudgetSlowBurn
        expr: |
          job:slo_errors_per_request:ratio_rate6h{job="results-api"} > (6 * 0.001)
          and
          job:slo_errors_per_request:ratio_rate30m{job="results-api"} > (6 * 0.001)
        labels:
          severity: ticket          # the Workbook pages here too; choose per service
        annotations:
          summary: "results-api has used 5% of its 30-day error budget in 6 hours"

Alertmanager тАФ pages PagerDuty рд▓рд╛, tickets chat рд▓рд╛, service рдиреБрд╕рд╛рд░ group рдХреЗрд▓реЗрд▓реЗ, рдЖрдгрд┐ рддреНрдпрд╛рдЪ service рдЪреЗ page рдЪрд╛рд▓реВ рдЕрд╕рддрд╛рдирд╛ ticket рдирд╛рд╣реА:

route:
  receiver: team-chat
  group_by: [alertname, job]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: ['severity="page"']
      receiver: pagerduty-results
receivers:
  - name: pagerduty-results
    pagerduty_configs:
      - routing_key: <PagerDuty Events API v2 integration key>
  - name: team-chat
    slack_configs:
      - api_url: <Slack incoming webhook URL>
        channel: "#results-alerts"
inhibit_rules:
  - source_matchers: ['severity="page"']
    target_matchers: ['severity="ticket"']
    equal: [job]

Datadog рдЪреЗ SLO monitors рд╣реАрдЪ рдХрд▓реНрдкрдирд╛ burn rate alert рдореНрд╣рдгреВрди рджреЗрддрд╛рдд (burn_rate("<slo id>").over("30d").long_window("1h").short_window("5m") > 14.4). CloudWatch composite alarm рдиреЗ alarms рдПрдХрддреНрд░ рдХрд░реВ рд╢рдХрддреЛ (ALARM(long) AND ALARM(short)), рдЖрдгрд┐ CloudWatch Application Signals рдордзреНрдпреЗ burn-rate alarms рд╕рд╣ SLOs рдЖрдзреАрдЪ рдЕрд╕рддрд╛рдд.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдорд╛рдЧрдЪреНрдпрд╛ рдорд╣рд┐рдиреНрдпрд╛рддреАрд▓ pages рдореЛрдЬрд╛. рдкреНрд░рддреНрдпреЗрдХрд╛рд╕рд╛рдареА рд╡рд┐рдЪрд╛рд░рд╛: "user рд▓рд╛ рддреЗ рдЬрд╛рдгрд╡рд▓реЗ рдХрд╛, рдЖрдгрд┐ рдХреБрдгрд╛рд▓рд╛ action рдШреНрдпрд╛рд╡реА рд▓рд╛рдЧрд▓реА рдХрд╛?" рдЬрд┐рдереЗ рдЙрддреНрддрд░ рдирд╛рд╣реА рд╣реЛрддреЗ рддреЗ рдкреНрд░рддреНрдпреЗрдХ page delete рдХрд░рд╛ рдХрд┐рдВрд╡рд╛ рдЦрд╛рд▓рдЪреНрдпрд╛ рд╕реНрддрд░рд╛рд╡рд░ рдиреНрдпрд╛.

тПня╕П рдкреБрдвреЗ

рднрд╛рдЧ 2 рдкреВрд░реНрдг рдЭрд╛рд▓рд╛. рдЗрдерд▓реНрдпрд╛ рдкреНрд░рддреНрдпреЗрдХ alert рдиреЗ "99.9%" рд╡рд╛рдкрд░рд▓реЗ. рд╣рд╛ рдЖрдХрдбрд╛ рдХреБрдареВрди рдпреЗрддреЛ, рдЖрдгрд┐ budget рд╕рдВрдкрд▓реНрдпрд╛рд╡рд░ рддреБрдореНрд╣реА рдХрд╛рдп рдХрд░рддрд╛? рднрд╛рдЧ 3 рд╕реБрд░реВ рд╣реЛрддреЛ.

git checkout lesson-09-slos

ЁЯЪи Lesson 08 тАФ Alerting: wake someone only when pupils are hurting

ЁЯУН You are here: Lesson 08 of 12 ┬╖ Previous: lesson-07-prometheus-grafana ┬╖ Next: lesson-09-slos


ЁЯУж What's in this branch

Lessons 01тАУ07, plus the end of Part 2: alerting. You learn to alert on symptoms (what users feel) and not causes (CPU), what a burn rate is, and the Google SRE Workbook's multi-window, multi-burn-rate alerts. burn_rate() and burn_alerts() live in obs/reliability.py; alerting() in obs/demo.py replays the whole 8-hour day through them.

ЁЯзТ Explain like I'm 5

The school has an alarm ЁЯЪи that wakes Katrina at night. Remember the bell on the boiler from lesson 01? It rang for 64 minutes and never once for an ill pupil. Katrina started to ignore it. That is the worst thing an alarm can do.

So Dipika writes new rules. The alarm listens to pupils, not the boiler:

And each rule checks two clocks. The long clock (the last hour) asks: "is this big enough to matter?" The short clock (the last 5 minutes) asks: "is it still happening?" Both must say yes. So a short sneeze does not ring the alarm, and the alarm stops soon after the pupils are well again.

ЁЯЧ║я╕П Diagram

flowchart TB
    err["ЁЯУИ error ratio per minute<br/>SLO 99.9% тЖТ budget rate 0.1%"]
    br["burn rate = error ratio ├╖ (1 тИТ SLO)<br/>0.9% тЖТ 9├Ч ┬╖ 6% тЖТ 60├Ч"]
    err --> br
    br --> pg{"ЁЯЪи PAGE<br/>тЙе 14.4├Ч over 1 h<br/>AND over 5 min"}
    br --> tk{"ЁЯУЭ TICKET<br/>тЙе 6├Ч over 6 h<br/>AND over 30 min"}
    pg -->|"bad deploy"| p1["minutes 409тАУ434<br/>9 min after it began"]
    tk -->|"slow leak"| t1["minutes 266тАУ390<br/>nobody woken"]
    cpu["ЁЯФХ CPU > 80%: 64 minutes of noise"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/observability/lesson-diagrams.html#l08

тЭУ What

ЁЯдФ Why

Because a page is expensive: it wakes a person, and every false one teaches them to ignore the next. CPU is a cause: it was above 80% for 64 minutes and parents were fine. Burn-rate alerts fire on what parents feel, scaled by how fast the budget is going. A big fast burn wakes someone within minutes. A small slow burn becomes a ticket. A short blip does nothing тАФ because it truly did not matter much.

ЁЯФз How (in this repo)

burn_rate(error_ratio, slo) in obs/reliability.py divides by the budget rate. window_ratio(errors, requests, end, minutes) is the error ratio of the last minutes before end. burn_alerts(errors, requests, slo=0.999) checks every minute: if the 1 h and 5 min burn rates are both тЙе 14.4 it pages; else if the 6 h and 30 min burn rates are both тЙе 6 it opens a ticket. (The else plays the part of an Alertmanager inhibit rule: no ticket while the page fires. The day starts at minute 0, so early in the day the 6 h window covers only the minutes so far.) alerting() replays the day: a slow leak (0.9%, minutes 100тАУ379) and a bad deploy (6%, minutes 400тАУ431). spans() in demo.py prints minute ranges.

ЁЯзк Try it

Shorten the bad deploy, then try a steady problem at two sizes:

python3 obs/demo.py alerting
python3 - <<'EOF'
import sys; sys.path.insert(0, "obs"); from demo import ERR, REQ, spans; from reliability import burn_alerts, burn_rate
print(f"burn rate of 0.9% errors on a 99.9% SLO: {burn_rate(0.009, 0.999):.0f}├Ч ┬╖ of 6%: {burn_rate(0.06, 0.999):.0f}├Ч")
for bad_minutes in (3, 10, 32):
    err = [1] * 480
    for m in range(400, 400 + bad_minutes): err[m] = 60
    pages, tickets = burn_alerts(err, REQ)
    print(f"6% errors for {bad_minutes:>2} min тЖТ page: {spans(pages)} ┬╖ ticket: {spans(tickets)}")
for pct in (1.5, 3.0):
    err = [1] * 480
    for m in range(100, 480): err[m] = int(pct * 10)
    pages, tickets = burn_alerts(err, REQ)
    print(f"{pct}% errors from minute 100 тЖТ first page {pages[0] if pages else 'never'} ┬╖ first ticket {tickets[0] if tickets else 'never'}")
EOF
python3 obs/test_obs.py

тЬЕ Verify тАФ what you should see

alerting prints:

тФАтФА 8 hours of traffic, SLO 99.9%: a slow leak (0.9% errors, minutes 100тАУ379) and a bad deploy (6%, minutes 400тАУ431)
   CPU > 80% alerts: 64 minutes of noise that do not track what users feel
   burn-rate TICKET (тЙе6├Ч over 6 h AND 30 min): minutes 266тАУ390, 401тАУ408, 435тАУ458 тАФ first the slow leak (nobody woken at night), then around the deploy while the page is not firing
   burn-rate PAGE (тЙе14.4├Ч over 1 h AND 5 min): minutes 409тАУ434 тАФ the bad deploy, 9 minutes after it began
   page for fast burns that hurt users now; open a ticket for slow burns; never page on a cause like CPU

Your snippet prints:

burn rate of 0.9% errors on a 99.9% SLO: 9├Ч ┬╖ of 6%: 60├Ч
6% errors for  3 min тЖТ page: never ┬╖ ticket: never
6% errors for 10 min тЖТ page: never ┬╖ ticket: never
6% errors for 32 min тЖТ page: minutes 413тАУ434 ┬╖ ticket: minutes 435тАУ458
1.5% errors from minute 100 тЖТ first page 157 ┬╖ first ticket 155
3.0% errors from minute 100 тЖТ first page 127 ┬╖ first ticket 120

The tests include тЬЕ L08 the bad deploy pages within 10 minutes; the slow leak only opens a ticket.

ЁЯПБ What you just proved

The slow leak (9├Ч) never paged тАФ it opened a ticket at minute 266. The bad deploy paged at minute 409, and the page stopped at 434, three minutes after the errors ended at 431 тАФ the 5-minute window at work. A 10-minute blip at 6% did nothing: it spent 600 ├╖ 43,200 тЙИ 1.4% of the month's budget, below the 2% that justifies a page. The same 32-minute deploy with no leak before it paged at 413 instead of 409 тАФ the leak had already used part of the last hour's budget. And a steady 3% (30├Ч) paged 27 minutes in; a steady 1.5% (15├Ч) took 57 minutes: the smaller the burn, the longer the 1-hour window needs.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ Prometheus recording rules for the error ratio over each window, then the Workbook's burn-rate alerts for a 99.9% SLO (0.001 is the budget rate):

groups:
  - name: results-api-slo
    rules:
      - record: job:slo_errors_per_request:ratio_rate5m
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))
      - record: job:slo_errors_per_request:ratio_rate30m
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[30m])) / sum by (job) (rate(http_requests_total[30m]))
      - record: job:slo_errors_per_request:ratio_rate1h
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[1h])) / sum by (job) (rate(http_requests_total[1h]))
      - record: job:slo_errors_per_request:ratio_rate6h
        expr: sum by (job) (rate(http_requests_total{status=~"5.."}[6h])) / sum by (job) (rate(http_requests_total[6h]))

      - alert: ResultsApiErrorBudgetFastBurn
        expr: |
          job:slo_errors_per_request:ratio_rate1h{job="results-api"} > (14.4 * 0.001)
          and
          job:slo_errors_per_request:ratio_rate5m{job="results-api"} > (14.4 * 0.001)
        labels:
          severity: page
        annotations:
          summary: "results-api is burning its 30-day error budget 14.4├Ч too fast (2% per hour)"
          runbook_url: https://runbooks.example.school/results-api/error-budget-burn

      - alert: ResultsApiErrorBudgetSlowBurn
        expr: |
          job:slo_errors_per_request:ratio_rate6h{job="results-api"} > (6 * 0.001)
          and
          job:slo_errors_per_request:ratio_rate30m{job="results-api"} > (6 * 0.001)
        labels:
          severity: ticket          # the Workbook pages here too; choose per service
        annotations:
          summary: "results-api has used 5% of its 30-day error budget in 6 hours"

Alertmanager тАФ pages to PagerDuty, tickets to chat, grouped per service, and no ticket while the page for the same service fires:

route:
  receiver: team-chat
  group_by: [alertname, job]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: ['severity="page"']
      receiver: pagerduty-results
receivers:
  - name: pagerduty-results
    pagerduty_configs:
      - routing_key: <PagerDuty Events API v2 integration key>
  - name: team-chat
    slack_configs:
      - api_url: <Slack incoming webhook URL>
        channel: "#results-alerts"
inhibit_rules:
  - source_matchers: ['severity="page"']
    target_matchers: ['severity="ticket"']
    equal: [job]

Datadog SLO monitors support the same idea as a burn rate alert (burn_rate("<slo id>").over("30d").long_window("1h").short_window("5m") > 14.4). CloudWatch can combine alarms with a composite alarm (ALARM(long) AND ALARM(short)), and CloudWatch Application Signals has SLOs with burn-rate alarms built in.

ЁЯПн Why this matters in production: count last month's pages. For each one, ask: "did a user feel it, and did a person need to act?" Delete or demote every page where the answer was no.

тПня╕П Next

Part 2 is done. Every alert here used "99.9%". Where does that number come from, and what do you do when the budget runs out? Part 3 begins.

git checkout lesson-09-slos
тЖР Previousprometheus grafanaNext тЖТslos

This page is the lesson's README from the lesson-08-alerting branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.