ЁЯПл The SchoolтА║ЁЯУР System DesignтА║ЁЯУИ рдзрдбрд╛ 15 тАФ рд░рдЪрдиреЗрддреВрдирдЪ observability: "рдирд┐рд░реЛрдЧреА" рдореНрд╣рдгрдЬреЗ рдХрд╛рдп рддреЗ рдард░рд╡рд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУИ рдзрдбрд╛ 15 тАФ рд░рдЪрдиреЗрддреВрдирдЪ observability: "рдирд┐рд░реЛрдЧреА" рдореНрд╣рдгрдЬреЗ рдХрд╛рдп рддреЗ рдард░рд╡рд╛

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 18 рдкреИрдХреА рдзрдбрд╛ 15 ┬╖ рдкреБрдвреЗ: lesson-16-cost-and-adrs


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

рдзрдбреЗ 01тАУ14, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ box рд╕рд╛рдареА рддрд┐рд╕рд░рд╛ рдкреНрд░рд╢реНрди: рддреЗ рдЪрд╛рд▓рддреЗ рдЖрд╣реЗ рд╣реЗ рдЖрдкрд▓реНрдпрд╛рд▓рд╛ рдХрд╕реЗ рдХрд│рдгрд╛рд░? рддреБрдореНрд╣реА рд╢рд┐рдХрддрд╛ SLI, SLO рдЖрдгрд┐ SLA, error budgets (рдорд╣рд┐рдиреНрдпрд╛рд▓рд╛ 432 / 43.2 / 4.3 рдорд┐рдирд┐рдЯреЗ), RED рдЖрдгрд┐ USE, request ids рд╕рд╣ structured logs, metrics, traces, рдЖрдгрд┐ рддреБрдореНрд╣реА рдХрд╛рд░рдгрд╛рдВрд╡рд░ рдирд╛рд╣реА рддрд░ рд▓рдХреНрд╖рдгрд╛рдВрд╡рд░ alert рдХрд╛ рдХрд░рддрд╛.

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

app рд╕реБрд░реВ рд╣реЛрдгреНрдпрд╛рдЖрдзреА рджреАрдкрд┐рдХрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рд▓рд╛ рд╡рд┐рдЪрд╛рд░рддреЗ: "рдирд┐рд░реЛрдЧреА рдореНрд╣рдгрдЬреЗ рдХрд╛рдп?"

рдХрддрд░рд┐рдирд╛ рдореНрд╣рдгрддреЗ: "рдЬрд▓рдж." рджреАрдкрд┐рдХрд╛ рдореНрд╣рдгрддреЗ: "рдХрд┐рддреА рдЬрд▓рдж, рдХреЛрдгрд╛рд╕рд╛рдареА, рдХрд┐рддреА рд╡реЗрд│рд╛?" рдореНрд╣рдгреВрди рддреНрдпрд╛ рдПрдХ рд╡рд╛рдХреНрдп рд▓рд┐рд╣рд┐рддрд╛рдд: "30 рджрд┐рд╡рд╕рд╛рдВрдд рдореЛрдЬрд▓реЗ рддрд░, рдкреНрд░рддреНрдпреЗрдХ 1,000 рдкреИрдХреА 999 feed reads 200 ms рдкреЗрдХреНрд╖рд╛ рдХрдореА рд╡реЗрд│рд╛рдд рдмрд░реЛрдмрд░ рдЙрддреНрддрд░ рджреЗрддрд╛рдд."

рдордЧ рдРрд╢реНрд╡рд░реНрдпрд╛ рдЧрдгрд┐рдд рдХрд░рддреЗ. рд╣рдЬрд╛рд░рд╛рдд рдПрдХ рдЕрдкрдпрд╢реА рдард░реВ рд╢рдХрддреЛ. 30 рджрд┐рд╡рд╕рд╛рдВрдд рддреЗ 43.2 рдорд┐рдирд┐рдЯрд╛рдВрдЪрд╛ рддреНрд░рд╛рд╕ рд╣реЛрддреЛ. рд╣реАрдЪ рдореБрднрд╛ тАФ error budget. рдЬрд░ app рдиреЗ рдпрд╛ рдорд╣рд┐рдиреНрдпрд╛рдд рдлрдХреНрдд 10 рдорд┐рдирд┐рдЯреЗ рд╡рд╛рдкрд░рд▓реА, рддрд░ team рдзреЛрдХрд╛ рдкрддреНрдХрд░реВ рд╢рдХрддреЗ: рдирд╡реА features, рдореЛрдареЗ рдмрджрд▓. рдЬрд░ рдореБрднрд╛ рдЬрд╡рд│рдЬрд╡рд│ рд╕рдВрдкрд▓реА рдЕрд╕реЗрд▓, рддрд░ team рд╡реЗрдЧ рдХрдореА рдХрд░рддреЗ рдЖрдгрд┐ рдЖрдзреА рдЧреЛрд╖реНрдЯреА рджреБрд░реБрд╕реНрдд рдХрд░рддреЗ.

рд╢реЗрд╡рдЯреА рдХрд╛рд░реНрдпрд╛рд▓рдп рдард░рд╡рддреЗ рдХреЛрдгрд╛рд▓рд╛ рдХрдзреА рдЭреЛрдкреЗрддреВрди рдЙрдард╡рд╛рдпрдЪреЗ. server рдЪрд╛ CPU рдЬрд╛рд╕реНрдд рдЕрд╕рддрд╛рдирд╛ рдирд╛рд╣реА тАФ рдкрд╛рд▓рдХрд╛рдВрдирд╛ CPU рдЬрд╛рдгрд╡рдд рдирд╛рд╣реА. рдлрдХреНрдд рддреЗрд╡реНрд╣рд╛рдЪ рдЬреЗрд╡реНрд╣рд╛ рдкрд╛рд▓рдХрд╛рдВрдирд╛ рдЬрд╛рдгрд╡рддреЗ: errors рдХрд┐рдВрд╡рд╛ рд╣рд│реВ pages, рдЬреЗ рдореБрднрд╛ рдЦреВрдк рд╡реЗрдЧрд╛рдиреЗ рд╕рдВрдкрд╡рдд рдЖрд╣реЗрдд.

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

flowchart LR
    sli["ЁЯУП SLI<br/>good reads ├╖ all reads"] --> slo["ЁЯОп SLO 99.9% / 30 days"]
    slo --> eb["тП│ error budget<br/>43.2 min per 30 days"]
    eb --> alert["ЁЯЪи alert on burn rate<br/>symptoms, not CPU"]
    subgraph sig["ЁЯУб three signals + one id"]
      logs["ЁЯУЬ logs<br/>JSON + request_id"]
      met["ЁЯУК metrics<br/>RED per API ┬╖ USE per resource"]
      tr["ЁЯз╡ traces<br/>one request across services"]
    end
    sig --> sli

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "рддреЗ рдЪрд╛рд▓рддреЗ рдЖрд╣реЗ рдХрд╛?" рдпрд╛рдЪреА рд╡реНрдпрд╛рдЦреНрдпрд╛ launch рдЪреНрдпрд╛ рдЖрдзреА рд╣рд╡реА, рдирд╛рд╣реАрддрд░ рдкреНрд░рддреНрдпреЗрдХ incident рд╡рд╛рджрд╛рдиреЗ рд╕реБрд░реВ рд╣реЛрддреЛ. SLO рдзрдбреЗ 10тАУ13 рд▓рд╛ рдЖрдХрдбреНрдпрд╛рдВрддрд╣реА рдмрджрд▓рддреЗ: рд░рдЪрдиреЗрд▓рд╛ рдХрд┐рддреА nines рд╣рд╡реЗрдд рддреНрдпрд╛рд╡рд░реВрди рдХрд┐рддреА рдкреНрд░рддреА, zones рдЖрдгрд┐ fallbacks рд▓рд╛рдЧрддреАрд▓ рддреЗ рдард░рддреЗ. рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ log рдУрд│реАрдд request id рдирд╕реЗрд▓, рддрд░ рдХрд╛рд░реНрдпрд╛рд▓рдп рдПрдХрд╛ рдкрд╛рд▓рдХрд╛рдЪреА рдЕрдкрдпрд╢реА request gateway, API, queue рдЖрдгрд┐ SMS worker рдордзреВрди рд╢реЛрдзреВ рд╢рдХрдд рдирд╛рд╣реА.

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

design/designs.py рдордзрд▓реЗ error_budget(slo, days=30) (1 тИТ slo) ├Ч days ├Ч 24 ├Ч 60 рдорд┐рдирд┐рдЯреЗ рдкрд░рдд рдХрд░рддреЗ, рдПрдХрд╛ рджрд╢рд╛рдВрд╢рд╛рдкрд░реНрдпрдВрдд round рдХреЗрд▓реЗрд▓реА. design/demo.py рдордзрд▓реЗ observability() рддреАрди SLOs рдЫрд╛рдкрддреЗ. snippet рдПрдХ рдЖрдард╡рдбрд╛ рдЖрдгрд┐ рдПрдХ рд╡рд░реНрд╖ рдЕрд╕реЗ рдХрд╛рд▓рд╛рд╡рдзреА рдЬреЛрдбрддреЛ, рдЖрдгрд┐ рдПрдХрд╛ рдорд╣рд┐рдиреНрдпрд╛рдЪреНрдпрд╛ рд╡рд╛рдИрдЯ reads рдиреЗ budget рдЪрд╛ рдХрд┐рддреА рднрд╛рдЧ рд╡рд╛рдкрд░рд▓рд╛ рддреЗ рдореЛрдЬрддреЛ.

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

python3 design/demo.py observability
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import error_budget
for slo in (0.99, 0.995, 0.999, 0.9995, 0.9999):
    print(f"SLO {slo:.2%} тЖТ {error_budget(slo):>6} min / 30 days ┬╖ {error_budget(slo, 7):>5} min / week ┬╖ {error_budget(slo, 365):>7} min / year")
good, total = 99_950_000, 100_000_000                 # a month of feed reads
slo = 0.999; allowed_bad = total * (1 - slo); bad = total - good
print(f"SLI {good / total:.3%} ┬╖ allowed bad {allowed_bad:,.0f} ┬╖ bad so far {bad:,} тЖТ {bad / allowed_bad:.0%} of the budget spent")
EOF

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

observability рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА decide what 'healthy' means BEFORE launch
   SLO 99.00% тЖТ error budget  432.0 minutes per 30 days
   SLO 99.90% тЖТ error budget   43.2 minutes per 30 days
   SLO 99.99% тЖТ error budget    4.3 minutes per 30 days
   RED per API (Rate, Errors, Duration) ┬╖ USE per resource (Utilisation, Saturation, Errors)
   logs with a request id ┬╖ metrics for alerts ┬╖ traces across services ┬╖ alert on the SLO, not on CPU

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

SLO 99.00% тЖТ  432.0 min / 30 days ┬╖ 100.8 min / week ┬╖  5256.0 min / year
SLO 99.50% тЖТ  216.0 min / 30 days ┬╖  50.4 min / week ┬╖  2628.0 min / year
SLO 99.90% тЖТ   43.2 min / 30 days ┬╖  10.1 min / week ┬╖   525.6 min / year
SLO 99.95% тЖТ   21.6 min / 30 days ┬╖   5.0 min / week ┬╖   262.8 min / year
SLO 99.99% тЖТ    4.3 min / 30 days ┬╖   1.0 min / week ┬╖    52.6 min / year
SLI 99.950% ┬╖ allowed bad 100,000 ┬╖ bad so far 50,000 тЖТ 50% of the budget spent

Tests рдордзреНрдпреЗ тЬЕ L15 a 99.9% SLO allows 43.2 minutes a month рдЖрд╣реЗ.

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

рдкреНрд░рддреНрдпреЗрдХ рдЬрд╛рд╕реНрддреАрдЪрд╛ nine рдЬрд╛рдЧрд╛ рджрд╣рд╛рдиреЗ рднрд╛рдЧрддреЛ: рдорд╣рд┐рдиреНрдпрд╛рд▓рд╛ 432 тЖТ 43.2 тЖТ 4.3 рдорд┐рдирд┐рдЯреЗ. 99.99% рд╡рд░ team рдХрдбреЗ рдЖрдард╡рдбреНрдпрд╛рд▓рд╛ рд╕рд╛рдзрд╛рд░рдг рдПрдХ рдорд┐рдирд┐рдЯ рдЕрд╕рддреЛ тАФ рдЪреБрдХрд▓реЗрд▓реНрдпрд╛ рдПрдХрд╛ deploy рдкреЗрдХреНрд╖рд╛рд╣реА рдХрдореА. рдореНрд╣рдгреВрдирдЪ SLO рдЧрд░рдЬреЗрд╢реА рдЬреБрд│рд╛рдпрд▓рд╛ рд╣рд╡реЗ (рд╕реВрдЪрдирд╛ рдлрд▓рдХрд╛рд╕рд╛рдареА 99.9% рдкреБрд░реЗрд╕реЗ), рдЗрдЪреНрдЫреЗрд╢реА рдирд╛рд╣реА. рдЖрдгрд┐ budget рдореЛрдЬрд▓реЗ рдЬрд╛рддреЗ, рдЕрдВрджрд╛рдЬрд╛рдиреЗ рдард░рдд рдирд╛рд╣реА: 100 million рдкреИрдХреА 50,000 рд╡рд╛рдИрдЯ reads рдореНрд╣рдгрдЬреЗ 99.95% SLI тАФ рдпрд╛ рдорд╣рд┐рдиреНрдпрд╛рдЪреЗ рдЕрд░реНрдзреЗ budget рдЦрд░реНрдЪ рдЭрд╛рд▓реЗ.

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

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

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдкреНрд░рддреНрдпреЗрдХ request рд╕рд╛рдареА рдПрдХ structured log рдУрд│ (рдкреНрд░рддреНрдпреЗрдХ service рд▓рд╛ рдорд┐рд│рд╛рд▓реЗрд▓рд╛ request_id рдХреЙрдкреА рдХрд░рддреЗ, рдХрд┐рдВрд╡рд╛ edge рд╡рд░ рдПрдХ рддрдпрд╛рд░ рдХрд░рддреЗ):

{"ts":"2026-09-27T09:00:01.234Z","level":"info","service":"notice-api","request_id":"req-7f3a9c",
 "route":"GET /parents/me/feed","status":200,"duration_ms":84,"cache":"hit"}

99.9% feed SLO рд╕рд╛рдареА Prometheus burn-rate alert. рдПрдХрд╛ рддрд╛рд╕рд╛рд╕рд╛рдареА 14.4 рдЪрд╛ burn rate 30 рджрд┐рд╡рд╕рд╛рдВрдЪреНрдпрд╛ budget рдЪреЗ 2% рдЦрд░реНрдЪ рдХрд░рддреЛ; рд▓рд╣рд╛рди window рдкрдг рддрдкрд╛рд╕рд▓реНрдпрд╛рдиреЗ рдЕрдбрдЪрдг рдерд╛рдВрдмрд▓реНрдпрд╛рд╡рд░ alert рд▓рд╡рдХрд░ рдерд╛рдВрдмрддреЛ:

groups:
- name: feed-slo
  rules:
  - record: feed:error_ratio:rate1h
    expr: sum(rate(http_requests_total{route="/parents/me/feed",code=~"5.."}[1h]))
        / sum(rate(http_requests_total{route="/parents/me/feed"}[1h]))
  - record: feed:error_ratio:rate5m
    expr: sum(rate(http_requests_total{route="/parents/me/feed",code=~"5.."}[5m]))
        / sum(rate(http_requests_total{route="/parents/me/feed"}[5m]))
  - alert: FeedErrorBudgetBurn
    expr: feed:error_ratio:rate1h > (14.4 * 0.001) and feed:error_ratio:rate5m > (14.4 * 0.001)
    for: 2m
    labels: { severity: page }

OpenTelemetry рд╕рд╣ traces тАФ API automatic instrumentation рд╕рд╣ рд╕реБрд░реВ рдХрд░рд╛ рдЖрдгрд┐ spans рдПрдХрд╛ collector рдХрдбреЗ рдкрд╛рдард╡рд╛:

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=notice-api OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
    opentelemetry-instrument python app.py

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдкреНрд░рддреНрдпреЗрдХ рд░рдЪрдиреЗрдд рдПрдХ "observability" рд╡рд┐рднрд╛рдЧ рдЬреЛрдбрд╛: SLIs, SLO рдЖрдгрд┐ рддреНрдпрд╛рдЪреЗ budget, RED рдЖрдгрд┐ USE dashboards, рдЖрдгрд┐ рдорд╛рдгрд╕рд╛рд▓рд╛ рдЙрдард╡реВ рд╢рдХрдгрд╛рд░реЗ рджреЛрди-рддреАрди alerts. рдмрд╛рдХреА рд╕рдЧрд│реЗ dashboard рдХрд┐рдВрд╡рд╛ ticket.

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

рдпреЛрдЬрдирд╛ рдмрд░реЛрдмрд░, рд╕реБрд░рдХреНрд╖рд┐рдд рдЖрдгрд┐ рдореЛрдЬрд▓реЗрд▓реА рдЖрд╣реЗ. рд╢рд╛рд│реЗрд▓рд╛ рддреА рдкрд░рд╡рдбреЗрд▓ рдХрд╛? рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ box рдХрд╛ рдЖрд╣реЗ рд╣реЗ рдХреЛрдгрд╛рд▓рд╛ рд▓рдХреНрд╖рд╛рдд рд░рд╛рд╣реАрд▓ рдХрд╛? Cost, trade-offs рдЖрдгрд┐ ADRs.

git checkout lesson-16-cost-and-adrs

ЁЯУИ Lesson 15 тАФ Observability by design: decide what "healthy" means

ЁЯУН You are here: Lesson 15 of 18 ┬╖ Next: lesson-16-cost-and-adrs


ЁЯУж What's in this branch

Lessons 01тАУ14, plus the third question for every box: how will we know it works? You learn SLI, SLO and SLA, error budgets (432 / 43.2 / 4.3 minutes a month), RED and USE, structured logs with request ids, metrics, traces, and why you alert on symptoms, not on causes.

ЁЯзТ Explain like I'm 5

Before the app opens, Dipika asks the office: "What does healthy mean?"

Katrina says: "Fast." Dipika says: "How fast, for whom, how often?" So they write one sentence: "999 of every 1,000 feed reads answer correctly in under 200 ms, counted over 30 days."

Then Aishwarya does the sum. One in a thousand may fail. Over 30 days that is 43.2 minutes of trouble. That is the allowance тАФ the error budget. If the app uses only 10 minutes this month, the team may take risks: new features, big changes. If the allowance is nearly gone, the team slows down and fixes things first.

Last, the office decides when to wake someone up. Not when a server's CPU is high тАФ parents do not feel CPU. Only when parents feel it: errors or slow pages, eating the allowance too fast.

ЁЯЧ║я╕П Diagram

flowchart LR
    sli["ЁЯУП SLI<br/>good reads ├╖ all reads"] --> slo["ЁЯОп SLO 99.9% / 30 days"]
    slo --> eb["тП│ error budget<br/>43.2 min per 30 days"]
    eb --> alert["ЁЯЪи alert on burn rate<br/>symptoms, not CPU"]
    subgraph sig["ЁЯУб three signals + one id"]
      logs["ЁЯУЬ logs<br/>JSON + request_id"]
      met["ЁЯУК metrics<br/>RED per API ┬╖ USE per resource"]
      tr["ЁЯз╡ traces<br/>one request across services"]
    end
    sig --> sli

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

тЭУ What

ЁЯдФ Why

Because "is it working?" needs a definition before launch, or every incident starts with an argument. The SLO also turns lessons 10тАУ13 into numbers: how many nines the design needs decides how many copies, zones and fallbacks it needs. And without a request id in every log line, the office cannot follow one parent's failed request through the gateway, the API, the queue and the SMS worker.

ЁЯФз How (in this repo)

error_budget(slo, days=30) in design/designs.py returns (1 тИТ slo) ├Ч days ├Ч 24 ├Ч 60 minutes, rounded to one decimal. observability() in design/demo.py prints three SLOs. The snippet adds periods of a week and a year, and measures how much of the budget one month's bad reads used.

ЁЯзк Try it

python3 design/demo.py observability
python3 - <<'EOF'
import sys; sys.path.insert(0, "design"); from designs import error_budget
for slo in (0.99, 0.995, 0.999, 0.9995, 0.9999):
    print(f"SLO {slo:.2%} тЖТ {error_budget(slo):>6} min / 30 days ┬╖ {error_budget(slo, 7):>5} min / week ┬╖ {error_budget(slo, 365):>7} min / year")
good, total = 99_950_000, 100_000_000                 # a month of feed reads
slo = 0.999; allowed_bad = total * (1 - slo); bad = total - good
print(f"SLI {good / total:.3%} ┬╖ allowed bad {allowed_bad:,.0f} ┬╖ bad so far {bad:,} тЖТ {bad / allowed_bad:.0%} of the budget spent")
EOF

тЬЕ Verify тАФ what you should see

observability prints:

тФАтФА decide what 'healthy' means BEFORE launch
   SLO 99.00% тЖТ error budget  432.0 minutes per 30 days
   SLO 99.90% тЖТ error budget   43.2 minutes per 30 days
   SLO 99.99% тЖТ error budget    4.3 minutes per 30 days
   RED per API (Rate, Errors, Duration) ┬╖ USE per resource (Utilisation, Saturation, Errors)
   logs with a request id ┬╖ metrics for alerts ┬╖ traces across services ┬╖ alert on the SLO, not on CPU

Your snippet prints:

SLO 99.00% тЖТ  432.0 min / 30 days ┬╖ 100.8 min / week ┬╖  5256.0 min / year
SLO 99.50% тЖТ  216.0 min / 30 days ┬╖  50.4 min / week ┬╖  2628.0 min / year
SLO 99.90% тЖТ   43.2 min / 30 days ┬╖  10.1 min / week ┬╖   525.6 min / year
SLO 99.95% тЖТ   21.6 min / 30 days ┬╖   5.0 min / week ┬╖   262.8 min / year
SLO 99.99% тЖТ    4.3 min / 30 days ┬╖   1.0 min / week ┬╖    52.6 min / year
SLI 99.950% ┬╖ allowed bad 100,000 ┬╖ bad so far 50,000 тЖТ 50% of the budget spent

The tests include тЬЕ L15 a 99.9% SLO allows 43.2 minutes a month.

ЁЯПБ What you just proved

Each extra nine divides the room by ten: 432 тЖТ 43.2 тЖТ 4.3 minutes a month. At 99.99% the team has about one minute a week тАФ less than one deploy that goes wrong. That is why the SLO must match the need (a notice board is fine at 99.9%), not a wish. And the budget is counted, not guessed: 50,000 bad reads of 100 million is an SLI of 99.95% тАФ half of this month's budget is spent.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ one structured log line per request (every service copies the request_id it receives, or creates one at the edge):

{"ts":"2026-09-27T09:00:01.234Z","level":"info","service":"notice-api","request_id":"req-7f3a9c",
 "route":"GET /parents/me/feed","status":200,"duration_ms":84,"cache":"hit"}

A Prometheus burn-rate alert for the 99.9% feed SLO. A burn rate of 14.4 for one hour spends 2% of a 30-day budget; checking a short window too makes the alert stop soon after the problem stops:

groups:
- name: feed-slo
  rules:
  - record: feed:error_ratio:rate1h
    expr: sum(rate(http_requests_total{route="/parents/me/feed",code=~"5.."}[1h]))
        / sum(rate(http_requests_total{route="/parents/me/feed"}[1h]))
  - record: feed:error_ratio:rate5m
    expr: sum(rate(http_requests_total{route="/parents/me/feed",code=~"5.."}[5m]))
        / sum(rate(http_requests_total{route="/parents/me/feed"}[5m]))
  - alert: FeedErrorBudgetBurn
    expr: feed:error_ratio:rate1h > (14.4 * 0.001) and feed:error_ratio:rate5m > (14.4 * 0.001)
    for: 2m
    labels: { severity: page }

Traces with OpenTelemetry тАФ start the API with automatic instrumentation and send spans to a collector:

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=notice-api OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 \
    opentelemetry-instrument python app.py

ЁЯПн Why this matters in production: add an "observability" section to every design: the SLIs, the SLO and its budget, the RED and USE dashboards, and the two or three alerts that may wake a person. Everything else is a dashboard or a ticket.

тПня╕П Next

The plan is correct, safe and measured. Can the school pay for it? And will anyone remember why each box is there? Cost, trade-offs and ADRs.

git checkout lesson-16-cost-and-adrs
тЖР PrevioussecurityNext тЖТcost and adrs

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