-
Notifications
You must be signed in to change notification settings - Fork 47
chore: add centralized docs #2802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Tofel
merged 3 commits into
dx-5122-alerts-assertion-p14
from
dx-5122-alerts-assertion-p15
Sep 9, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,46 @@ | ||
| # grafana-alertcheck | ||
|
|
||
| A CD quality gate for Grafana alerts: bookend a release with `watch` (record) and `check` (classify) to | ||
| answer whether any watched alert was in a bad state during the release window. | ||
| A CD quality gate for Grafana alerts. It bookends a release with two commands β `watch` (record) and | ||
| `check` (classify) β and answers whether any watched alert was in a bad state during the release window. | ||
|
|
||
| Under construction. | ||
| ``` | ||
| watch β your work β check | ||
| ``` | ||
|
|
||
| `watch` starts a background recorder that polls each named alert into a JSONL log. After the work emits a | ||
| `from`/`to` pair, `check` proves continuous coverage of that window, classifies each alert's state | ||
| timeline, and exits `0`, `1`, or `2`. | ||
|
|
||
| It **fails closed**: if it cannot get an answer, it stops the release β never a pass on an unproven window. | ||
|
|
||
| ## Quickstart | ||
|
|
||
| ```bash | ||
| export GRAFANA_URL=https://grafana.example.com | ||
| export GRAFANA_TOKEN=β¦ | ||
|
|
||
| grafana-alertcheck watch --out /tmp/run.jsonl --alerts alerts.txt | ||
| ./deploy.sh # emits deployed_at=<RFC3339> | ||
| ./verify.sh # emits finished_at=<RFC3339> | ||
| grafana-alertcheck check --in /tmp/run.jsonl --from "$deployed_at" --to "$finished_at" | ||
| ``` | ||
|
|
||
| Requires Grafana >= 13.0.0 and < 14.0.0. Connection details come from the environment only β the token is | ||
| never a flag. | ||
|
|
||
| ## Documentation | ||
|
|
||
| | Doc | Covers | | ||
| | --- | ------ | | ||
| | [`docs/index.md`](./docs/index.md) | Overview, quickstarts, exit codes, common surprises | | ||
| | [`docs/how-alerts-are-evaluated.md`](./docs/how-alerts-are-evaluated.md) | Verdict model, coverage proof, health/liveness | | ||
| | [`docs/advanced.md`](./docs/advanced.md) | Check budget, scheduling, why history isn't queried | | ||
| | [`docs/architecture.md`](./docs/architecture.md) | Design invariants, the pure-function seam, recorder lifecycle | | ||
| | [`docs/reference/cli.md`](./docs/reference/cli.md) | Full CLI reference β subcommands, flags, naming | | ||
| | [`docs/reference/log-format.md`](./docs/reference/log-format.md) | The JSONL log schema, for debugging artifacts | | ||
|
|
||
| ## Build | ||
|
|
||
| ```bash | ||
| go build ./... && go test ./... | ||
| ``` | ||
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
File renamed without changes.
File renamed without changes.
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
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
File renamed without changes.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| position: 1 | ||
| label: 'Grafana Alertcheck' | ||
| collapsible: true | ||
| collapsed: false | ||
| link: | ||
| type: generated-index | ||
| slug: /platform-services/devex/cicd/grafana-alertcheck/index | ||
| description: 'CD quality gate for Grafana alerts: watch, classify, and gate releases.' |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| --- | ||
| id: grafana-alertcheck-advanced | ||
| title: Check budget and scheduling | ||
| sidebar_label: Budget and scheduling | ||
| sidebar_position: 2 | ||
| description: Why grafana-alertcheck schedules per rule, how the request budget works, and why it never queries state history. | ||
| --- | ||
|
|
||
| # Check budget and scheduling | ||
|
|
||
| ## Per-rule schedules, never a global cycle | ||
|
|
||
| Each rule polls at its **own** cadence, `--poll-interval` (default: half the rule's own evaluation interval). There is deliberately no single global minimum-interval cycle. | ||
|
|
||
| One rule at `intervalSeconds=10` beside twenty at `300` keeps a 5 s cadence for itself and 150 s for the other twenty β not a 5 s cycle for all of them, which would be a 60Γ request bloat at ~1.8 s per request and would fail to start on a reasonable fleet. | ||
|
|
||
| The scheduler staggers each rule's initial next-due time across its cadence, and serves due rules **earliest-due-first**, so a tight rule never queues behind slack ones. | ||
|
|
||
| ## The check budget | ||
|
|
||
| The gate records one observation of every rule up front and checks the schedule against those **measured** latencies (payload sizes vary ~230Γ across rules, so a fixed estimate is meaningless). It errors at start β before waiting β if any of three conditions hold: | ||
|
|
||
| - **Utilization** β total request rate exceeds `--concurrency`. | ||
| - **Per-rule** β one rule's request can't fit its own cadence. | ||
| - **Burst bound** β the slowest request exceeds the fleet's tightest cadence, which can open a mid-run gap. | ||
|
|
||
| The error names the three levers only: raise `--concurrency`, raise `--poll-interval`, or watch fewer alerts. It never prescribes a single interval. | ||
|
|
||
| ## Why the gate never queries state history | ||
|
|
||
| Querying Grafana's alert state history after the fact fails closed *in the wrong direction* β it returns "pass" when the truth is unknown: | ||
|
|
||
| - History stores **transitions**, not states. An alert firing through the whole window has its only record *before* the window. | ||
| - The annotations API **does not serve Loki-backed history** at all. | ||
| - An empty result is indistinguishable from a healthy one: no alert fired, the backend differs, retention removed data, the token lacked permission β all look identical. | ||
| - There is **no coverage signal** β nothing proves the history is complete to time T. | ||
| - Artifact transitions (`Paused`, `RuleDeleted`, `Updated`, `MissingSeries`) look like recoveries. | ||
|
|
||
| Instead, `watch` records its own evidence live and the log becomes the source of truth. The trade-off: the gate can miss an episode shorter than a rule's poll interval, though `activeAt` still surfaces sub-interval onsets for instances still active at a poll. | ||
|
|
||
| A corollary of recording fresh: there is no replay. Re-running a failed job is a new deploy with a new `from` and a new recording β never a re-classification of old evidence. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| --- | ||
| id: grafana-alertcheck-architecture | ||
| title: Architecture | ||
| sidebar_label: Architecture | ||
| sidebar_position: 3 | ||
| description: The design invariants, pure-function seam, and recorder lifecycle of grafana-alertcheck, for maintainers. | ||
| --- | ||
|
|
||
| # Architecture | ||
|
|
||
| This page documents the invariants and seams a maintainer must not break. It exists because most of them are the difference between a gate that fails closed and one that silently passes broken windows. | ||
|
|
||
| ## Fail-closed invariants | ||
|
|
||
| The gate must stop the release if it cannot get an answer. Every rule below is a specific instance of that: | ||
|
|
||
| - **An error is never a pass.** A pass is exactly `len(Violations) == 0 && err == nil`. Every error path leaves `err` non-nil, and the CLI maps that to exit `2` unconditionally. | ||
| - **Inability beats violation.** Any `unobservable` rule is exit `2`, even alongside a real violation found first. | ||
| - **Absent never means normal.** An instance that leaves the bad set is looked up in the *same* response: present as `normal` β cleared; absent (or `MissingSeries`) β vanished (a discontinuity, not a recovery). | ||
| - **Staleness is absolute.** `grafana_now β lastEvaluation` is compared against a threshold, never "did it increase since the last poll" β a delta check reports stale on ~half the polls of a healthy rule. | ||
| - **`grafana_now` is the response `Date` header.** Never the runner clock, in any comparison against a Grafana timestamp. | ||
| - **No early exit.** `check` collects to `to + transitionGrace` before classifying once. | ||
| - **No replay.** No run-id key, no artifact download, no state between attempts. A retry is a new deploy. | ||
|
|
||
| ## The pure-function seam | ||
|
|
||
| All correctness lives in two phases written as **pure functions** over a flat list of polls β no HTTP, no files, no clock, no goroutines: | ||
|
|
||
| ``` | ||
| HTTP ββ> Source ββ> []StateRule ββ> reduce ββ> []Poll ββ> proveCoverage ββ> decide ββ> Result | ||
| β | ||
| JSONL log ββ> ReadLog βββ | ||
| ``` | ||
|
|
||
| - `proveCoverage` (the nine coverage checks) and `decide` (the instance timelines and outcomes) are pure; tests drive them with `[]Poll` literals and a fake `Clock`, with no sleeping or fixture server. | ||
| - `Check`/`Watch` are I/O shells: HTTP, signals, the pidfile, file reads, the countdown print. The only test doubles needed are the `Source` and `Clock` interfaces. | ||
| - `Policy` is the narrowed view of `Config` that reaches the pure layer β classification knobs and the window, no URL and no token. The token must never cross that line, which is the cheapest guarantee it never lands in an error string or a result. | ||
|
|
||
| ## Strict parsing as the version guard | ||
|
|
||
| Both API responses are parsed strictly: a missing or unparseable **required** field (`health`, `state`, `lastEvaluation`, `interval`) is an error, never a zero value. Optional keys (`alerts`, `totals`, `labels`, `keepFiringFor`) are absent-tolerant, and unknown keys are ignored β so Grafana can add fields without breaking the parser, but removing one fails loudly. | ||
|
|
||
| This, plus the declared supported range (Grafana >= 13.0.0, < 14.0.0), is how a deprecation or schema change is caught instead of silently misread. | ||
|
|
||
| ## The recorder lifecycle | ||
|
|
||
| `watch` detaches a background recorder so observation survives the step boundary: | ||
|
|
||
| 1. Parent resolves names, writes the header, observes every non-paused rule once, checks the budget. | ||
| 2. Parent re-execs itself as the child (`--daemon-child`) under a new session/process group, stdout/stderr to the daemon log. | ||
| 3. Child re-reads the header, reopens the log `O_APPEND`, takes the exclusive `flock`, and writes one readiness byte on `--ready-fd`. | ||
| 4. Parent writes the pidfile **after** the readiness report, then returns. | ||
|
|
||
| Two authorities, only one of which is evidence: | ||
|
|
||
| - The **pidfile** says a recording ever started (written only after ready, removed on failure). It can go stale β a pid gets reused. | ||
| - The **flock** says a writer exists *now*. The kernel drops it on exit, so the lock is always authoritative. | ||
|
|
||
| On a clean stop (SIGTERM/SIGINT/`--until`) the child finishes the in-flight write, appends the `stopped` sentinel, fsyncs, and exits. A hard error writes no sentinel β so a recorder that died reads exactly like a coverage gap, because it is one. | ||
|
|
||
| `check` signals via the pidfile, waits for the **lock** to release (never the pid), and only then reads the log once. Reading while a writer can still append can only produce a shorter window than was recorded. | ||
|
|
||
| ## The log is the source of truth | ||
|
|
||
| `watch` records raw evidence, so nothing trusts a state that could become unreachable. Two consequences a maintainer must preserve: | ||
|
|
||
| - The **header is authoritative for recording facts** (the cadence actually used, the URL, the alert set); the ruler API is authoritative for **rule facts** (`for`, `intervalSeconds`, kind). `check` always re-resolves definitions fresh and never reconstructs them from the header β the header duplicates `for`/`interval` only so the uploaded artifact is self-describing. | ||
| - The **cadence authority** is the header's `poll_every_seconds`, not the definitions. Re-deriving it would compare gaps recorded at an override cadence against default-cadence thresholds β fail-open in the faster-override direction. |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.