e2e-alertmanager-test
Takes the exp_alerts fixtures out of a Prometheus promtool-style unit
test file, POSTs each one to a real Alertmanager's /api/v2/alerts
endpoint, and renders what the resulting notification would look like in
several formats. It does not test routing/inhibition logic inside this
tool itself — it exercises whatever routing your target Alertmanager
instance is actually configured with, and shows you the rendered output.
When to use this skill
- Before merging an alert change, to see the actual notification content
(subject line, body, Slack attachment) a human on-call would receive —
not just "does it fire," but "does it read well."
- As a CI step against a throwaway/ephemeral Alertmanager container,
producing a rendered-output artifact that reviewers can diff between
runs, the same way a UI snapshot test is diffed. Fed a skeleton config
(see below), this run is fully hermetic — see Achieving full
hermeticity in CI.
- This tool's own external dependency is the Alertmanager instance, not
historical data — unlike
alert-hysteresis/stale-alerts-analyzer, it
needs no real production history, so a fresh, disposable Alertmanager
instance is sufficient and the run is fully reproducible.
Prerequisites
- A Prometheus unit-test file with
exp_alerts fixtures — typically
the output of autogen-promql-tests (see that skill) with real,
filled-in labels/annotations, or a hand-written promtool test file.
Test cases whose exp_alerts is empty (asserting "does not fire") are
silently skipped — there's nothing to render for those.
- A running Alertmanager reachable over HTTP, defaulting to
http://localhost:9093. For CI or local preview, run one disposably:docker run -d -p 9093:9093 prom/alertmanager
Achieving full hermeticity in CI
The e2e-alertmanager-test binary itself is never hermetic on its own —
it always needs to POST to a real, reachable Alertmanager. But the
routing config that Alertmanager instance loads is entirely under your
control, and that's where every external dependency can be removed:
- Treat the team's real
alertmanager.yml — the one that governs
production route: and inhibit_rules: — as the source of truth for
routing/grouping/inhibition logic. That's the logic worth exercising
in a test.
- Build a skeleton config for CI that keeps that routing tree intact
but overrides every receiver's connection details —
smtp_smarthost,
Slack api_url, PagerDuty/webhook URLs and keys — with local, no-op
values. For example, point smtp_smarthost at a local SMTP catcher
(e.g. MailHog/smtp4dev) instead of the real relay, so the receiver
still accepts a connection and the message headers can be rendered and
diffed, but no mail actually leaves the runner.
- Start the ephemeral Alertmanager for the CI job with the skeleton
config, not the real one, and point
--alertmanager-url at it.
With this setup, the Alertmanager under test still runs the real
routing/grouping/inhibition rules — so a routing regression still
surfaces in the diff — but delivery stops at the local catcher: no real
email, Slack message, or PagerDuty incident is ever created. That's what
makes this tool's CI output (rendered notification content, including
SMTP headers) safe to treat as a fully hermetic, deterministic artifact.
Generating and maintaining the skeleton config (importing the team's
routing tree while stripping receiver secrets) is on you — this repo
doesn't ship tooling for that merge/override step, and --alertmanager-config
(below) does not perform it either; it only enriches this tool's own
rendered output text.
Keep the skeleton config's routing tree in sync with the real one, ideally
generated from it rather than hand-maintained — if it drifts, a green CI
run stops guaranteeing anything about production routing.
Setup
go build -o bin/e2e-alertmanager-test ./cmd/e2e-alertmanager-test
# or: go install github.com/conallob/o11y-analysis-tools/cmd/e2e-alertmanager-test@latest
Usage
e2e-alertmanager-test [options]
| Flag |
Default |
Effect |
--tests |
(required) |
Path to the Prometheus unit-test YAML file to replay. |
--alertmanager-url |
http://localhost:9093 |
Alertmanager API base URL to POST alerts to. |
--alertmanager-config |
"" |
Optional path to alertmanager.yml — used only to enrich rendered output (e.g. smtp_from, receiver name in the routing-info footer), not to actually apply routing rules locally. |
--output |
email |
One of email, email-html, slack, json, full. |
--full |
false |
Also render the HTML body inside email/email-html output. |
--verbose |
false |
Print the Alertmanager URL being posted to per alert. |
Typical invocations
# Plain-text email preview
e2e-alertmanager-test --tests=./alerts_test.yml --output=email
# Full HTML email rendering
e2e-alertmanager-test --tests=./alerts_test.yml --output=email-html --full
# Slack attachment JSON preview
e2e-alertmanager-test --tests=./alerts_test.yml --output=slack
# Everything, redirected to a diffable artifact for CI
e2e-alertmanager-test --tests=./alerts_test.yml --output=full > notifications.txt
Reading the output
Each test case with non-empty exp_alerts becomes one numbered "Test #N",
printing the alert name and then the rendered output in the requested
format(s), followed by a summary:
═══════════════════════════════════════════════════════════
Summary
═══════════════════════════════════════════════════════════
Total test cases: 3
Successful: 3
Failed: 0
Exit code 1 if any alert failed to POST successfully (e.g. Alertmanager
unreachable or returned non-200), 0 otherwise. Note: "successful" here
means the POST to Alertmanager succeeded, not that routing/inhibition
matched any particular expectation — this tool doesn't assert routing
outcomes, it only renders notification content.
Agent workflow
- Confirm a unit-test file with real (non-
TODO) exp_alerts exists —
if not, point the user at autogen-promql-tests first.
- Confirm an Alertmanager is reachable at
--alertmanager-url (start a
disposable one via Docker if none is running and this is a local/CI
context). Never point this at a production Alertmanager — it really
posts alerts, and a receiver with live connection details will really
dispatch them. For CI, load the disposable instance with a skeleton
config that keeps real
routing/grouping/inhibition but swaps receiver connection details for
local no-op ones, so the run stays hermetic even though it exercises
real routing logic.
- Run with
--output=full to capture every rendering for a
comprehensive review, or --output=slack/--output=email-html when a
user specifically wants to review one channel's format.
- For CI: redirect output to a file and store it as a build artifact so
subsequent runs can be diffed to catch unintended notification-content
regressions from template/label changes.
1---2name: e2e-alertmanager-test3description: Render end-to-end previews of what an alert notification will actually look like (plain-text email, HTML email, Slack attachment JSON, raw webhook JSON) by replaying a Prometheus unit-test file's expected alerts through a live Alertmanager. Effectively a "print preview" for alerts. Use to review notification formatting/content before merging alert changes, or in CI against an ephemeral Alertmanager to produce a diffable preview artifact. Requires a reachable Alertmanager API and a promtool-style unit-test file as input. Can be made fully hermetic in CI by pointing the ephemeral Alertmanager at a skeleton config that keeps the team's real routing/grouping/inhibition rules but overrides receiver connection details, so no real notification ever leaves the runner.4license: BSD-3-Clause5---67# e2e-alertmanager-test89Takes the `exp_alerts` fixtures out of a Prometheus `promtool`-style unit10test file, POSTs each one to a real Alertmanager's `/api/v2/alerts`11endpoint, and renders what the resulting notification would look like in12several formats. It does not test routing/inhibition logic inside this13tool itself — it exercises whatever routing your target Alertmanager14instance is actually configured with, and shows you the rendered output.1516## When to use this skill1718- Before merging an alert change, to see the actual notification content19 (subject line, body, Slack attachment) a human on-call would receive —20 not just "does it fire," but "does it read well."21- As a CI step against a **throwaway/ephemeral** Alertmanager container,22 producing a rendered-output artifact that reviewers can diff between23 runs, the same way a UI snapshot test is diffed. Fed a skeleton config24 (see below), this run is fully hermetic — see [Achieving full25 hermeticity in CI](#achieving-full-hermeticity-in-ci).26- This tool's own external dependency is the Alertmanager instance, not27 historical data — unlike `alert-hysteresis`/`stale-alerts-analyzer`, it28 needs no real production history, so a fresh, disposable Alertmanager29 instance is sufficient and the run is fully reproducible.3031## Prerequisites32331. **A Prometheus unit-test file** with `exp_alerts` fixtures — typically34 the output of `autogen-promql-tests` (see that skill) with real,35 filled-in labels/annotations, or a hand-written `promtool` test file.36 Test cases whose `exp_alerts` is empty (asserting "does not fire") are37 silently skipped — there's nothing to render for those.382. **A running Alertmanager** reachable over HTTP, defaulting to39 `http://localhost:9093`. For CI or local preview, run one disposably:40 ```bash41 docker run -d -p 9093:9093 prom/alertmanager42 ```4344## Achieving full hermeticity in CI4546The `e2e-alertmanager-test` binary itself is never hermetic on its own —47it always needs to POST to a real, reachable Alertmanager. But the48routing config *that Alertmanager instance loads* is entirely under your49control, and that's where every external dependency can be removed:50511. Treat the team's real `alertmanager.yml` — the one that governs52 production `route:` and `inhibit_rules:` — as the source of truth for53 routing/grouping/inhibition logic. That's the logic worth exercising54 in a test.552. Build a **skeleton config** for CI that keeps that routing tree intact56 but overrides every receiver's connection details — `smtp_smarthost`,57 Slack `api_url`, PagerDuty/webhook URLs and keys — with local, no-op58 values. For example, point `smtp_smarthost` at a local SMTP catcher59 (e.g. MailHog/smtp4dev) instead of the real relay, so the receiver60 still accepts a connection and the message headers can be rendered and61 diffed, but no mail actually leaves the runner.623. Start the ephemeral Alertmanager for the CI job with the skeleton63 config, not the real one, and point `--alertmanager-url` at it.6465With this setup, the Alertmanager under test still runs the real66routing/grouping/inhibition rules — so a routing regression still67surfaces in the diff — but delivery stops at the local catcher: no real68email, Slack message, or PagerDuty incident is ever created. That's what69makes this tool's CI output (rendered notification content, including70SMTP headers) safe to treat as a fully hermetic, deterministic artifact.71Generating and maintaining the skeleton config (importing the team's72routing tree while stripping receiver secrets) is on you — this repo73doesn't ship tooling for that merge/override step, and `--alertmanager-config`74(below) does not perform it either; it only enriches this tool's own75rendered output text.7677Keep the skeleton config's routing tree in sync with the real one, ideally78generated from it rather than hand-maintained — if it drifts, a green CI79run stops guaranteeing anything about production routing.8081## Setup8283```bash84go build -o bin/e2e-alertmanager-test ./cmd/e2e-alertmanager-test85# or: go install github.com/conallob/o11y-analysis-tools/cmd/e2e-alertmanager-test@latest86```8788## Usage8990```91e2e-alertmanager-test [options]92```9394| Flag | Default | Effect |95|---|---|---|96| `--tests` | *(required)* | Path to the Prometheus unit-test YAML file to replay. |97| `--alertmanager-url` | `http://localhost:9093` | Alertmanager API base URL to POST alerts to. |98| `--alertmanager-config` | `""` | Optional path to `alertmanager.yml` — used only to enrich rendered output (e.g. `smtp_from`, receiver name in the routing-info footer), not to actually apply routing rules locally. |99| `--output` | `email` | One of `email`, `email-html`, `slack`, `json`, `full`. |100| `--full` | `false` | Also render the HTML body inside `email`/`email-html` output. |101| `--verbose` | `false` | Print the Alertmanager URL being posted to per alert. |102103### Typical invocations104105```bash106# Plain-text email preview107e2e-alertmanager-test --tests=./alerts_test.yml --output=email108109# Full HTML email rendering110e2e-alertmanager-test --tests=./alerts_test.yml --output=email-html --full111112# Slack attachment JSON preview113e2e-alertmanager-test --tests=./alerts_test.yml --output=slack114115# Everything, redirected to a diffable artifact for CI116e2e-alertmanager-test --tests=./alerts_test.yml --output=full > notifications.txt117```118119## Reading the output120121Each test case with non-empty `exp_alerts` becomes one numbered "Test #N",122printing the alert name and then the rendered output in the requested123format(s), followed by a summary:124125```126═══════════════════════════════════════════════════════════127Summary128═══════════════════════════════════════════════════════════129Total test cases: 3130Successful: 3131Failed: 0132```133134Exit code `1` if any alert failed to POST successfully (e.g. Alertmanager135unreachable or returned non-200), `0` otherwise. Note: "successful" here136means the POST to Alertmanager succeeded, not that routing/inhibition137matched any particular expectation — this tool doesn't assert routing138outcomes, it only renders notification content.139140## Agent workflow1411421. Confirm a unit-test file with real (non-`TODO`) `exp_alerts` exists —143 if not, point the user at `autogen-promql-tests` first.1442. Confirm an Alertmanager is reachable at `--alertmanager-url` (start a145 disposable one via Docker if none is running and this is a local/CI146 context). **Never point this at a production Alertmanager** — it really147 posts alerts, and a receiver with live connection details will really148 dispatch them. For CI, load the disposable instance with a [skeleton149 config](#achieving-full-hermeticity-in-ci) that keeps real150 routing/grouping/inhibition but swaps receiver connection details for151 local no-op ones, so the run stays hermetic even though it exercises152 real routing logic.1533. Run with `--output=full` to capture every rendering for a154 comprehensive review, or `--output=slack`/`--output=email-html` when a155 user specifically wants to review one channel's format.1564. For CI: redirect output to a file and store it as a build artifact so157 subsequent runs can be diffed to catch unintended notification-content158 regressions from template/label changes.