Alerting

An alerting pipeline is only worth having if you still read it

The hard part isn't sending an alert. It's making sure the one that arrives at 1am is worth getting out of bed for.

The shape

An alert rule in git fires and goes to Alertmanager, which fans out three ways to reach me and a fourth to reach the agent. The three are an amber ntfy push for criticals only, an alerts channel and a security channel. The fourth is the alert-detector, drawn blue as the gate: it runs hourly, only for allow-listed alert names, posts once to Hermes, and Hermes replies in the alerts channel after opening a pull request.
Two exits. One wakes me. The other only ever opens a pull request, so it lands in the same review queue with the same human at the end of it — just started without me being awake.

Rules are files in the repository, evaluated by Alertmanager. Routes, receivers and inhibit rules are one reviewed object. A change to any of it is a pull request like everything else, which means the alerting config has a history and a reviewer instead of being a thing someone fixed in a hurry.

Where everything actually shows up

Discord is the entire human interface for this cluster. Not a dashboard I check — I would not check it. Channels, each with a single job, so that a policy finding and a paging incident are not sitting in the same thread arguing about what they mean.

Five producers in a column on the left post into four Discord channels in a column on the right, drawn green: alerts, security, pull-requests and general. Alertmanager feeds both alerts and security. Flux feeds alerts and Alertmanager feeds alerts, so the two lines converge on that one channel. The GitHub webhook and CI both feed pull-requests, converging there. Hermes, drawn blue, feeds general.
Five producers, four channels. The four on the left are channel webhooks: each can add a message to the one channel it was created in and nothing else. Hermes is the odd one out.

Three of the four channels are backed by a webhook each, and a webhook is bound to the channel it was created in. That is why two producers sharing a channel have to share a webhook — Flux posts through Alertmanager's, and CI through the same one the GitHub webhook uses — and why the lines above converge in pairs. Fewer credentials to hold is the point. The cost is that losing one webhook silences one channel carrying two producers' output, not one producer's. Hermes is the exception: it holds an app token rather than a channel webhook, so it is scoped to the channels it was granted instead of being confined to one.

Why a webhook is a good credential here

Everything posting in holds one channel webhook and nothing else. A webhook can add a message to the one channel it was created in: it cannot read history, cannot react, cannot join another channel, and no API client anywhere holds it. There is one consequence worth knowing — a channel webhook is permanently bound to its channel, so the security channel's webhook cannot be swapped for the alerts channel's URL. That separation is a property of the credential, not a routing rule somebody has to remember to maintain.

The agent's way out is the opposite shape: one Discord App token, minted and re-minted hourly by a sidecar the model's own container never holds. It can post, and that is very nearly all it can do. The agent is told to reply in channel, and told never to merge. A rule the model can be argued out of is not a control — which is why the credential matters as much as the prompt: this one can post, and nothing else. It is not what stops a merge, though. The branch rules are, and those are covered under the loop.

Severity decides who hears it

Warnings group. Criticals page. The two alert receivers are sibling routes with continue, so each alert fans out to both independently and one dying doesn't silence the other. When a receiver did break, that turned "paging is down" into "half of paging is down" — a much smaller problem. The alert also names the leg that failed, so I don't have to deduce it from silence.

If everything pages, nothing does. That is the whole discipline, and it is much easier to state than to hold: every alert added here has to be one I would rather stop reading than miss.

The alert that proves the pipeline works by firing

There's an alert that always fires. It isn't noise, it's a dead man's switch: its absence is the signal that the rules stopped loading, the scrape died, or the pipeline can't deliver. Any alerting system should have one, routed to a receiver you've proven works.

Silences follow the same rule. A mute that lives only in the Alertmanager API doesn't survive a rebuild of the pipeline. If a maintenance window has to be declarative, it's a route in git matched on every shape the host appears as, with a written reason and an expiry.

Alertmanager can't report its own death. Every notification it would send travels through it, so a rule that fires on an unhealthy Alertmanager is delivered by the unhealthy Alertmanager. The external probe has the same hole: it can see the host go down, and the only thing that could tell anyone is the host that went down.

So there is a scheduled job that runs outside the pipeline and posts straight to the push service — a different process on a different machine, which survives the failure it is reporting. It checks two things: that Alertmanager still answers its readiness endpoint, and that the always-firing rule above is still firing in Prometheus. That second one matters more than it looks: from inside, a Prometheus that has stopped evaluating is indistinguishable from one with nothing to report. If either check fails, the phone buzzes by a path that does not involve Alertmanager at all. That is the break glass.

The agent is the second responder, not the pager

The cluster can wake the agent on its own. A scheduled job reads what's currently firing from Alertmanager and posts the new alerts to a webhook on the agent, signed, in-cluster only. It wakes up knowing what broke, looks at the resources and logs involved, and answers in the channel. If the fix is a manifest change, it opens a pull request and stops.

It can't be pushed to directly: Alertmanager can't sign an inbound request, so that job is the sender, and it only ever forwards what Alertmanager itself is already firing. Nothing on the network can hand the agent a task.

The controls on that path matter, because this is the thing that spends money:

When that path first ran, it was the dominant cost, so it was turned off and rebuilt narrower before being turned back on. The measurement that stopped it was the whole provider account for 24 hours, not this path.

The habits that keep it trustworthy

What I've decided is not an alert

Each of those is written down where the rule lives, with the date, so the next person to see the alert doesn't re-litigate it.

A firing alert. One notification per problem, grouped, with a link to the graph — an alert that doesn't link to something actionable gets ignored.