Sign in
← Observability
11 min · medium · Lesson 5 of 6

Alerts: rules and Alertmanager

How metrics turn into alerts: alert rules, routing and grouping in Alertmanager, and cutting down the noise.

Why alerts

A dashboard from the previous lesson is useful when someone is looking at it. But at three in the morning nobody is looking, and the service can still be down. An alert is an automatic notification: the system notices a problem in the metrics by itself and wakes the on-call engineer. Without alerts, observability is passive; with alerts, it reacts.

Alert rule

An alert is a condition on a metric that holds for some time. In Prometheus, rules are written in PromQL from the metrics lesson, in a separate YAML file:

groups:
  - name: shop
    rules:
      - alert: HighErrorRate
        expr: >
          sum(rate(http_requests_total{status=~"5.."}[5m]))
          / sum(rate(http_requests_total[5m])) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "5xx share above 5% for five minutes"

Rules are collected into groups (groups), and each rule has its own fields:

for: 5m cuts off short spikes. Without it, the alert would fire on a one-second blip and wake someone for nothing. When the condition holds for 5 minutes, the problem is real.

The expression above computes the error share for the whole store. If there are several services, sum by (service) (...) is better: then the alert is separate for each service, with a service label, and the on-call engineer immediately sees which one is failing. You can insert this label into the annotation text:

        annotations:
          summary: "{{ $labels.service }}: 5xx share {{ $value | humanizePercentage }}"

$labels holds the series labels, $value holds the expression's value at the moment of the check.

Alert lifecycle

Prometheus checks the rules on the period set by evaluation_interval in its own config (once a minute by default). Every alert has three states:

StateWhat it means
inactivethe rule is loaded, the condition isn't met
pendingthe condition is met, but for hasn't elapsed yet
firingthe condition has held longer than for, the alert goes to Alertmanager

If the condition goes away while the alert is pending, it goes back to inactive and never fires. That's how for damps spikes. The states show up on Prometheus's /alerts page, the loaded rules on /rules.

How Prometheus finds rules

Prometheus learns which files to read from the rule_files section of its config:

rule_files:
  - rules/*.yml

These are path patterns, and a relative path is resolved from the folder where prometheus.yml lives. A pattern that matches no files isn't treated as an error: Prometheus starts and runs without rules. So after an edit, check that the rules were actually loaded. You can see that on /rules or in promtool's output:

promtool check config prometheus.yml
Checking prometheus.yml
  SUCCESS: 1 rule files found
 SUCCESS: prometheus.yml is valid prometheus config file syntax

Checking rules/alerts.yml
  SUCCESS: 1 rules found

If the rule files found line is missing, no rule file was found at all.

Prometheus rereads changed rules without a restart: on a SIGHUP signal, or on a POST /-/reload request if it's started with the --web.enable-lifecycle flag. If the new file has an error, it keeps the old rules and writes the error to its log.

Alertmanager: what to do with an alert

Prometheus decides that an alert fired, and Alertmanager decides what to do with it: who to send it to, how, when. It's a separate program. Prometheus learns where to send alerts from the alerting section of its config:

alerting:
  alertmanagers:
    - static_configs:
        - targets: ["alertmanager:9093"]
Prometheus (rule fired) → Alertmanager → Slack / PagerDuty / email

Alertmanager handles four things.

Routes and grouping are set in Alertmanager's config as a route tree:

route:
  receiver: team-chat
  group_by: [alertname, service]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: [severity="critical"]
      receiver: oncall-pager

receivers:
  - name: team-chat
  - name: oncall-pager

An alert travels down the tree and lands in the first route that matches it, or goes to the root receiver (team-chat) if none matched. Real receivers also have channel settings (slack_configs, pagerduty_configs, email_configs, and others), left out here. group_by says which alerts to merge into one notification. group_wait is how long to wait for the rest of the group's alerts before the first notification, group_interval is how often to report new alerts in an already-sent group, repeat_interval is how long to wait before reminding if the alert is still firing. The values in the example match the defaults.

Fighting noise

The main enemy of alerting is noise. If there are too many alerts and some of them are false, the on-call engineer stops reacting to them (alert fatigue), and the important alert drowns. So alerts are kept few and meaningful:

Example symptom: "payments are slow." The average response time is misleading here. If 90% of requests are fast and 10% take 2 seconds, the average looks fine, while every tenth user is waiting. That's why a latency alert is set on a percentile (p95, p99), computed from a histogram with the histogram_quantile function from the metrics lesson.

A good set of alerts is often built around the same golden signals and SLOs from the next lesson.

Common mistakes

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson observability-basics-05-alerting