slo-proof

Replays past SLI data through a proposed alert rule before it goes live. For each rule it reports which incidents it would have caught and how late, how much error budget burned before it paged, and how often it would have woken someone for nothing.

Alert rules are usually tested by waiting. Ship the rule, then learn at 3 a.m. that it pages on every overnight traffic dip, or learn in a post-mortem that it slept through the slow burn that ate half the month's budget. The data to answer both questions is already in your metrics.

View on GitHub How it decides

Plays the seven stages below in order, about 75 seconds. Scroll or press Esc to stop.

Language
Python 3.11+
Tests
71 passing, the evaluator checked against a reference
Dependencies
None at runtime, standard library only
Inputs
A per-minute CSV, an incidents CSV, rules in TOML
Prometheus
from-prom converts two range-query exports
License
MIT
slo-proof compare examples/sli.csv examples/incidents.csv examples/rules.toml 28.0 days, 6 incidents that deserved a page, SLO 99.9%

All three are real runs of slo-proof 0.1.0 on the repo's example month. Refused feeds it two small files written for this page: a CSV with a missing minute and a rules file with a misspelt key.

1Refuse a gap instead of guessing

The SLI is one CSV row per minute: good events and total events. Incidents are a second CSV, and the rules are TOML. Every file is parsed strictly, and a bad one stops the run with exit code 2 and a message that names the file and, for the CSVs and the rules, the line.

  • Rows must sit on minute boundaries, carry a timezone, and run one minute apart. A missing minute could mean no traffic or a dead exporter, and the two give different answers, so a quiet minute has to be written as a row of zeros.
  • An unknown key in the rules is an error. A misspelt windows would otherwise make a rule that silently never fires.
  • A burn rate that needs an error ratio above 100% is refused, and so are overlapping incidents, because a page during both could not be attributed to either.

Rules errors give the line of the key at fault, and also name the rule and the condition, so a typo in a long file is one jump away.

Five files it won't replay Illustrative input

    The five files were written for this page. Each error line is exactly what slo-proof 0.1.0 printed to stderr, and every run exited 2 without replaying anything. The two TOML files are shown from line 5, after their [slo] table.

    2Evaluate every minute, the way Prometheus would

    The rule is evaluated at the end of every minute. A window of W minutes covers the last W complete minutes, and its error ratio is bad events over total events, what sum(increase(bad[W])) / sum(increase(total[W])) gives over per-minute recording rules.

    • for works as in Prometheus: the first true evaluation makes the alert pending, it fires at the first evaluation at least for later, and every evaluation in between must hold. A false one resets the clock.
    • A window with no traffic has no ratio and never fires. In PromQL 0/0 is NaN, and NaN > x is false.
    • Each transition to firing is one page. Alertmanager's grouping and repeat intervals change how often you are reminded, not when the first page goes out, so they are left out.

    One stated simplification: Prometheus's increase() extrapolates to the window's edges, while slo-proof sums whole minutes exactly. The difference is a fraction of a minute's events at each edge.

    A 4-minute outage against for = "5m"

    Rulestatic-1pct: error ratio above 1% over 5m, for 5m

    Hard outage, 13:41 to 13:45PendingFiring

    Minutes 13:36 to 13:51 on 4 March from the committed example month, with each evaluation computed by slo-proof's own holds() and evaluate(). Bars are the share of that minute's requests that failed. The row for fast-burn-only is the same data under the second rule in examples/rules.toml.

    3Give a long window a short one to reset it

    A rule fires if any of its when conditions holds, and a condition holds only if the error ratio is above its threshold over every one of its windows. That one rule shape covers a static threshold, a single-window burn rate and the SRE Workbook's multiwindow, multi-burn-rate alert.

    • A burn rate is a multiple of the budget rate. At a 99.9% target the budget rate is 0.1% errors, so 6x means 0.6%.
    • A 6-hour window remembers a 4-minute outage for 6 hours. Alone, it keeps firing long after users stopped noticing.
    • Pairing it with a 30-minute window, as the Workbook recommends, lets the alert clear soon after recovery.

    The eight hours after the outage

    6x-over-6h
    firing 6h00m
    workbook-multiwindow
    firing 33m
    Window ratios sampled every 5 minutes from the committed example month, and firing spans from slo-proof's evaluate(). The outage lasted from 13:41 to 13:45. Over the month this one shape costs 6x-over-6h 18h43m of firing with no incident open.

    4Score every page against what happened

    This is the line. Before a rule can wake anyone, each page it would have sent is held against the incidents you know about. A page during an incident is true. One within an hour after an incident ended is late: about something real, but nobody could act on it. Anything else is false.

    • Incidents marked page are the ones a rule has to catch. ticket incidents were real but could wait: a page during one isn't false, and missing one isn't held against the rule.
    • An incident is caught if the rule is firing at any moment while it is open. The delay runs from the incident's start, and a late page catches nothing.
    • Firing with no incident open is counted in time as well as pages, which is where a long window without a short one shows its cost.

    The incident records are taken as true. If a recorded start is when someone noticed rather than when users did, every delay is measured from the wrong moment, which is why the repo ships a synthetic month where the truth is known.

    Every page static-1pct would have sent in 28 days

    True
    8
    Late
    2
    False
    3

    The first false page: 8 March, 02:00 to 02:40, requests per minute

    Pages and their classes come from slo-proof's score() on the committed example month. Incidents are drawn above the axis, pages below it. Traffic in the month averages 548 requests a minute; during the three night dips it falls to about 6 while health checks fail, which the synthetic month adds on purpose.

    ClearedAbove this line slo-proof only replays the past. Below it, each rule leaves with its record: what it missed, what the delay cost, and an exit code that can keep it off the pager.

    5Price every delay in error budget

    Catching an incident isn't the whole answer. For each incident the replay reports the bad events from its start until the page, as a share of the period's error budget at the data's average traffic, next to what the whole incident cost.

    • The budget is one SLO period's allowance of bad events: (1 - target) times average traffic per minute times the minutes in the period, 30 days unless the rules say otherwise.
    • A missed incident spends its whole cost before anyone is told.

    The slow burn: 0.7% errors for 8 hours

    Incident
    8h
    Paged after
    4h53m
    Spent before the page
    5.5%
    Spent in all
    10.2%
    Rules that missed it
    2 of 4
    Budget spent and firing spans are computed by slo-proof's own score() and evaluate() on 18 March of the committed example month; the four percentages match slo-proof replay. Even the rule that catches everything lets 5.5% of the month's budget go first. The table says so before an incident does.

    6Rank the rules on the same month

    compare replays every rule in the file and ranks them: incidents caught first, then false pages per week, then median delay. It is a default order, not a verdict, and the table carries the numbers for any other.

    • Precision counts true pages over all pages, so late pages lower it as well as false ones.
    • Under the table, each rule that missed something lists what it missed by name.

    Four rules from examples/rules.toml, ranked

    Rows enter in the order the rules file defines them, then sort into the order slo-proof compare printed. Every number is from that run, verbatim.

    7Fail the change before the rule goes live

    With --fail-on-miss, replay and compare exit 1 when a rule misses an incident that deserved a page. A pull request that changes an alert rule can then be gated in CI on last quarter's incidents.

    • Exit 0: every page-worthy incident caught. 1: something was missed, with --fail-on-miss. 2: bad input.
    • The replay covers the past. A rule that caught last quarter's incidents can still miss a new kind, which is what the synthetic month is for.

    Three runs, three exit codes

    Real runs on the committed example month, output verbatim and exit codes from the shell. The bad input is the gap file from stage 1. Choose a card to see its run.

    Not one lucky month

    slo-proof synth builds a month with a daily and weekly traffic rhythm, Poisson noise, and one of each kind of trouble that tells rules apart. The seed moves every incident by up to an hour. The cards below replay the four example rules on seeds 1 to 8 with the real CLI.

    Same misses, every seed

    On all eight months, the multiwindow rule caught all six page-worthy incidents with no false pages, and fast-burn-only missed exactly the slow burn.

    8 of 8 seeds6/6 caught0.0 false a week

    The static threshold missed the hard outage, the slow burn and the short blip on every seed. A repo test checks the same claims on the same eight seeds.

    Firing with no incident

    Hours 6x-over-6h spent firing with no incident open, by seed.

    Every month: over 18 hours. The multiwindow rule stayed between 1h38m and 1h41m.

    The traffic dips

    Each synthetic month has three overnight dips where traffic falls to about 6 requests a minute while health checks fail. A 1% static threshold reads them as outages.

    Seven seeds0.8/wk
    Seed 41.0/wk

    False pages a week from static-1pct. The dip on 8 March spent 0.19% of the month's budget.

    71 tests, no network

    The evaluator is checked against a second implementation written another way: it sums each window by hand and fires wherever the expression held at every evaluation of the last for minutes. Hypothesis feeds both up to 400 random series and rules, with up to three conditions of up to three windows each.

      Weighted toward refusing bad input

      • Parsers: 43 cases of input that must be refused: gaps, duplicates, missing timezones, typos in rule keys, impossible burn rates, overlapping incidents, malformed Prometheus exports. Each checks the message and the line number, or that there is none when the problem is the whole file or a Prometheus export's contents.
      • Evaluation: firing at once with no for, a false evaluation resetting the pending clock, still firing at the end of the data, all windows required, any condition enough, no traffic never firing.
      • Properties: alerts come out ordered and disjoint, and a longer for never pages more.
      • Scoring: true, late and false pages on a built timeline, detection when the rule was already firing, budget arithmetic, ranking order.
      • The example month: it is what synth --seed 7 writes, and the README's findings hold on eight seeds.

      CI runs the tests on Python 3.11 and 3.13, ruff and mypy --strict, and checks that the committed examples and pictures match what the current code generates.

      Deliberately missing

      The future
      It replays the past. A rule that would have caught last quarter's incidents can still miss a new kind; the synthetic month covers kinds your history hasn't produced yet.
      Alertmanager
      Inhibition, grouping, silences and repeat intervals are outside the model, and so is evaluation jitter.
      Extrapolation
      Window sums are exact whole minutes. For windows of an hour or more the difference is negligible; for a 1-minute window it isn't.
      Several SLIs at once
      One series per run. Rules that combine services or route by label need one replay per series.

      Run it

      git clone https://github.com/ahmed-hashim-pro/slo-proof && cd slo-proof
      pip install -e .
      
      slo-proof compare examples/sli.csv examples/incidents.csv examples/rules.toml
      slo-proof replay  examples/sli.csv examples/incidents.csv examples/rules.toml --rule workbook-multiwindow
      slo-proof check   examples/rules.toml
      slo-proof synth   --seed 3 --out mymonth/
      
      # Tests
      pip install -e '.[dev]'
      pytest

      Python 3.11 or newer, in a virtualenv. The tool itself has no dependencies; the dev extra adds pytest, Hypothesis, ruff and mypy, all pinned.