“Last Tuesday's new alert never fired. It was never loaded — the config reload had been failing silently for six days.”
case name: Who monitors the monitor?
- Service
- prometheus · alertmanager
- Impact
- Six days with alert rules out of date; one real incident missed
- Detected by
- The incident review asking 'why didn't the new alert fire?'
- Time to resolve
- 6 days undetected
Skills you'll use on this case
promtool check in CI00 It starts
Reproduce this incident in your lab, then work the case alongside the timeline below. Try each query yourself before reading its result.
01 The investigation
- Tue
ACTION
A teammate adds a new alert and pushes the config; the deploy script runs
curl -X POST /-/reloadand ignores the response - Mon
REPORT
Review: the alert added last Tuesday should have fired twice. It didn't exist in Prometheus at all.
- Mon
QUERY
Prometheus reports its own config health
Prometheus is itself instrumented.
prometheus_config_last_reload_successfulhas been 0 since Tuesday;..._success_timestamp_secondsshows exactly when the last good reload happened.PromQL· Prometheusprometheus_config_last_reload_successful (time() - prometheus_config_last_reload_success_timestamp_seconds) / 86400result{instance="localhost:9090", job="prometheus"} 0 {instance="localhost:9090", job="prometheus"} 6.2 (days since the last successful reload) - Mon
QUERY
…and whether rule evaluation or notification is failing
PromQL· Prometheussum by (rule_group) (rate(prometheus_rule_evaluation_failures_total[5m])) > 0 or rate(prometheus_notifications_errors_total[5m]) > 0resultEmpty query result (healthy — only the reload was broken) - Mon
RESOLVED
Config fixed; meta-alerts, a Watchdog heartbeat, and CI validation added
Root cause
A syntax error made every config reload fail. Prometheus correctly kept running the last good config, so all existing dashboards and alerts looked normal. Nothing monitored the monitoring system's own health, and the deploy process didn't check the reload result.
02 The concepts behind it
Failure modes of the monitoring system
Config or rules fail to load (this case). Rule evaluation fails (bad query, too slow). Prometheus is down or OOM (Case 1.2). Prometheus can't reach Alertmanager. Alertmanager can't reach the pager. The pager integration's credentials expired. Each of these produces the same visible symptom: silence, which looks exactly like 'everything is fine'.
Meta-monitoring
Alert on the stack's own metrics: prometheus_config_last_reload_successful == 0, rule evaluation failures, notification errors, dropped alerts, alertmanager_notifications_failed_total, plus up for every component (Loki, Tempo, Alloy, the collector). Ideally a SECOND, independent Prometheus (or a managed service) scrapes the first, since a dead Prometheus can't alert about its own death.
The dead man's switch
The only way to detect 'the whole alerting path is broken' is to expect a signal CONSTANTLY and alert on its ABSENCE, from outside. A Watchdog rule with expr: vector(1) is always firing. Alertmanager sends it every minute to an external heartbeat service (Healthchecks.io, PagerDuty/Opsgenie heartbeats, Grafana OnCall). If the heartbeat stops (Prometheus down, rules broken, Alertmanager down, network cut), THAT service pages you.
03 The fix
01Meta-alerts on Prometheus and Alertmanager
obs-lab/prometheus/alerts.ymladd to fileyaml - name: meta rules: - alert: PrometheusConfigReloadFailed expr: prometheus_config_last_reload_successful == 0 for: 10m labels: { severity: critical, service: observability } annotations: summary: "Prometheus config reload failing — running on stale rules" - alert: PrometheusRuleEvaluationFailures expr: sum by (rule_group) (rate(prometheus_rule_evaluation_failures_total[5m])) > 0 for: 10m labels: { severity: warning, service: observability } - alert: AlertmanagerNotificationsFailing expr: sum by (integration) (rate(alertmanager_notifications_failed_total[5m])) > 0 for: 10m labels: { severity: critical, service: observability } - alert: Watchdog expr: vector(1) labels: { severity: none, service: observability } annotations: summary: "Always firing. If this stops arriving at the heartbeat service, alerting is broken."02Route the Watchdog to an external heartbeat
Add a scrape job for
alertmanager:9093too, so its metrics exist. The Watchdog route repeats every minute; the heartbeat service is configured to alert if it misses, say, 5 minutes of check-ins. In the lab, the alert-logger stands in for it.obs-lab/alertmanager/alertmanager.ymladd to fileyaml routes: - matchers: [alertname="Watchdog"] receiver: heartbeat group_wait: 0s group_interval: 1m repeat_interval: 1m # ...existing critical → pager route receivers: - name: heartbeat webhook_configs: [{ url: "http://alert-logger:8080/heartbeat" }] # real life: https://hc-ping.com/<uuid>
04 Make sure it never surprises you again
01Validate before you reload — in CI
promtool check configvalidates prometheus.yml and every referenced rules file;amtool check-configdoes the same for Alertmanager. Run both, plus thepromtool test rulesfrom Case 4.4, on every pull request, and make the deploy script fail if/-/reloaddoesn't return 200.terminal$ docker compose exec prometheus promtool check config /etc/prometheus/prometheus.yml── expected output ──Checking /etc/prometheus/prometheus.ymlSUCCESS: 2 rule files foundSUCCESS: /etc/prometheus/prometheus.yml is valid prometheus config file syntaxChecking /etc/prometheus/alerts.ymlSUCCESS: 21 rules found
05 Your turn: write the query
Run each one against your lab before revealing the answer. Share your own version in the comments; there's usually more than one correct query.
Write a query that shows every component of the lab's observability stack that Prometheus can't scrape.
Why can't a single Prometheus reliably alert that it's down?
06 Interview questions from this case
How do you make sure your alerting system is working?