feat(slo): reusable multiwindow multi-burn-rate SLO alerting - #12
Merged
Merged
Conversation
Adds `slo/`, extracted from two production consumers (pay-core and blockchain-api) that
had each grown a copy of the same file. This is the shared home so they stop diverging.
slo/burn_rate.libsonnet the mechanics: tier table, budget sizing, PromQL conditions
slo/rule.libsonnet tiers + SLIs -> Grafana Unified Alerting rule groups
slo/panels.libsonnet the burn-rate chart and the error-budget bar gauge
slo/tests/ promtool suites over the shipping expressions
instant_query.libsonnet make an alert rule's query instant, not range
The two consumers differed in exactly two things once compared, so those are the only
parameters: the label carrying the priority (`priority` vs `og_priority`) and two
sentences of annotation prose. Everything else was identical. Every knob is a hidden
field, so a consumer retunes with `burnRate + { tiers:: [...] }` rather than forking.
Also adds the repo's first CI. A shared library that nobody tests is worse than a copy
per repo, because a regression now lands everywhere at once. The suites exercise the
MECHANICS against fixture SLIs — window arithmetic, budget sizing, the noise floor,
expression splicing, the sparse-denominator hazard, and that the panels agree with the
rules about the size of the budget. Whether a given repo's objective is the right number
stays that repo's business.
Two bugs the extraction surfaced, both now pinned by fixtures:
- A fixture whose good/bad counters share a label set made `events()` fail
immediately. Both consumers selected the two counters with one `__name__` matcher,
which works only because one of their metrics happens to carry an extra label:
`increase()` drops `__name__`, so otherwise the two collapse to the same label set
and Prometheus rejects the vector, taking every SLO rule into `exec_err_state` at
once. The fixtures deliberately use identical label sets so nobody can reintroduce
it.
- `sum(good) + sum(bad)` — the other obvious form — yields nothing when either side
has no series, which is precisely a dimension in total outage.
Existing consumers pin a submodule SHA, so nothing moves until they bump it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
geekbrother
approved these changes
Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extracted from
pay-coreandblockchain-api, which had each grown a copy of the sameburn-rate implementation. This is the shared home so they stop diverging — I created that
divergence last week by copying the file, so this closes it.
What's here
slo/burn_rate.libsonnetslo/rule.libsonnetslo/panels.libsonnetslo/tests/instant_query.libsonnet.github/workflows/ci.ymlWhy so little is parameterised
I diffed the two copies before extracting. Ignoring comments and whitespace, they
differed in two things: the label carrying the priority (
priorityvsog_priority) and two sentences of annotation prose. So those are the onlyopts; therest was already identical. Every knob is a hidden field, so a consumer retunes with
burnRate + { tiers:: [...] }instead of forking.Two bugs the extraction surfaced
Writing fixtures that didn't share the consumers' incidental metric shapes broke the
code immediately — which is the argument for a library having its own tests.
events()depended on a label-set accident. Both copies selected the good and badcounters with a single
__name__matcher.increase()drops__name__, so the twocollapse to the same label set and Prometheus rejects the vector — unless some other
label distinguishes them. In
blockchain-apione metric happens to carryprovider_kind. Remove it and every SLO rule goes toexec_err_statetogether, froman unrelated metrics refactor. Fixed with the tag-and-union idiom
max_ofalreadyuses; the fixtures deliberately give good/bad identical label sets so it cannot
come back. (Fixed in blockchain-api separately.)
sum(good) + sum(bad)— the form I tried first — yields nothing when eitherside has no series, which is exactly a dimension in total outage.
Testing
CI installs go-jsonnet (matching Terraform's
alxrem/jsonnetprovider, which embeds it)and promtool, renders rules and both panels from fixtures, then evaluates 3 suites
(~4s). The suites test mechanics — window arithmetic, budget sizing, the noise floor,
expression splicing, the sparse-denominator hazard, panel/rule agreement. Every case is
mutation-checked.
Not a breaking change: consumers pin a submodule SHA, so nothing moves until they
bump it. Follow-ups will migrate
blockchain-apiandpay-coreonto this and deletetheir local copies.
🤖 Generated with Claude Code