Command Palette

Search for a command to run...

Hectal
Case 5.1·Production ObservabilitySEV2

“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

meta-monitoringreload and rule-evaluation healththe Watchdog / dead man's switch patternpromtool check in CI

00 It starts

Reproduce this incident in your lab, then work the case alongside the timeline below. Try each query yourself before reading its result.

terminal
$ # break the rules file: indent one line wrongly, or add "expr: rate(" and save
curl -s -X POST localhost:9090/-/reload
── lab output ──
failed to reload config: one or more errors occurred while applying the new configuration (--config.file="/etc/prometheus/prometheus.yml")
Prometheus keeps running with the OLD rules. Nothing on any dashboard changes. That's the problem.

01 The investigation

  1. Tue

    ACTION

    A teammate adds a new alert and pushes the config; the deploy script runs curl -X POST /-/reload and ignores the response

  2. Mon

    REPORT

    Review: the alert added last Tuesday should have fired twice. It didn't exist in Prometheus at all.

  3. Mon

    QUERY

    Prometheus reports its own config health

    Prometheus is itself instrumented. prometheus_config_last_reload_successful has been 0 since Tuesday; ..._success_timestamp_seconds shows exactly when the last good reload happened.

    PromQL· Prometheus
    prometheus_config_last_reload_successful
    (time() - prometheus_config_last_reload_success_timestamp_seconds) / 86400
    result
    {instance="localhost:9090", job="prometheus"}  0
    {instance="localhost:9090", job="prometheus"}  6.2   (days since the last successful reload)
  4. Mon

    QUERY

    …and whether rule evaluation or notification is failing

    PromQL· Prometheus
    sum by (rule_group) (rate(prometheus_rule_evaluation_failures_total[5m])) > 0
    or
    rate(prometheus_notifications_errors_total[5m]) > 0
    result
    Empty query result   (healthy — only the reload was broken)
  5. 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

  1. 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."
  2. 02Route the Watchdog to an external heartbeat

    Add a scrape job for alertmanager:9093 too, 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

  1. 01Validate before you reload — in CI

    promtool check config validates prometheus.yml and every referenced rules file; amtool check-config does the same for Alertmanager. Run both, plus the promtool test rules from Case 4.4, on every pull request, and make the deploy script fail if /-/reload doesn't return 200.

    terminal
    $ docker compose exec prometheus promtool check config /etc/prometheus/prometheus.yml
    ── expected output ──
    Checking /etc/prometheus/prometheus.yml
    SUCCESS: 2 rule files found
    SUCCESS: /etc/prometheus/prometheus.yml is valid prometheus config file syntax
     
    Checking /etc/prometheus/alerts.yml
    SUCCESS: 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.

01

Write a query that shows every component of the lab's observability stack that Prometheus can't scrape.

02

Why can't a single Prometheus reliably alert that it's down?

06 Interview questions from this case

01

How do you make sure your alerting system is working?

0/4 · 0%