ObservabilityXVIII · Alerting RulesAlertingRules
Severity and Routing Labels
What you'll learn
- Apply a three-tier severity scheme (info, warning, critical) consistently across an alert estate
- Match Alertmanager routes on severity and team labels to direct alerts to the correct receiver
- Identify the cost of a single severity scheme when the on-call rotation grows or splits
- Distinguish private severity (driving internal paging) from public severity (driving status-page classification)
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A team rebuilds its route tree after six months of production. The
cumulative cost of the old tree shows up in two numbers: how many
severity: critical pages the on-call rota received that should
have been tickets, and how many severity: critical pages never
reached a human because the route match silently failed. Both
numbers are non-zero in every estate that grew without a labelled
contract. Severity and routing labels are that contract.
What it is
Routing labels are the dimensions an Alertmanager route tree matches on to decide who receives a given alert. The two labels that almost every production route tree depends on are:
severity— the operational priority of the alert. Common values:info,warning,critical. The label drives whether the route ends at a page, a ticket, or a chat notification.team— the owning team. Common values: the team name in lowercase (checkout,platform,data). The label drives which rota or which Slack channel receives the alert.
Two further labels appear in most estates but are not strictly required:
service— the owning service (often identical toteamin small shops, diverging as the org scales).envorstage—prod,staging,dev. Used to keep non-production alerts off the production route.
The labels are set on the Prometheus rule itself, not on the
metric. The expr returns metric series with whatever labels the
exporter set; the alert rule adds labels: at the rule top level,
which are merged into the final alert.
Why a sysadmin cares
Severity is the contract between the rule author and the on-call.
A rule marked severity: critical says: someone will be paged at
03:00 for this. A rule marked severity: warning says: this is
ticket-shaped; we want it visible but not waking anyone. If the
labels do not match what the route tree expects, the contract
breaks silently. The on-call rota may be over-paged by tickets, or
under-paged by real incidents.
The team label exists for the same reason. A page that arrives in the wrong team’s inbox is a 15-minute delay. The cost of a wrong team label compounds with every misroute.
How it works
Alertmanager evaluates alerts against a routing tree. The tree is
matched top-down; the first route whose matchers: block matches
the alert’s labels wins, and the alert is sent to that route’s
receiver. A continue: true on a route lets siblings also
match.
Prometheus Alertmanager
----------- ------------
alert fires ----> route tree
labels: |
severity=critical +-- root (catch-all)
team=checkout | |
| +-- match severity=critical
| | -> pager-primary
| +-- match team=checkout
| | -> slack-checkout
| +-- match severity=warning
| -> jira-tickets
+-- default -> email-oncall
The matchers block is a list of label matchers. Three forms:
matchers:
- severity = "critical"
- severity =~ "crit|high"
- service != "test-payments"
Equality, regex, and inequality. Matchers within a single route are AND-ed; an alert matches the route only when every matcher matches.
How to configure it
Two sides to this: what the rule sets, and what Alertmanager matches.
The rule side. Add a stable set of labels to every rule:
groups:
- name: orders-api.slo
rules:
- alert: OrdersApiHighErrorRate
expr: |
sum by (service, region) (
rate(http_requests_total{service="orders-api", status=~"5.."}[5m])
)
/
sum by (service, region) (
rate(http_requests_total{service="orders-api"}[5m])
)
> 0.05
for: 5m
labels:
severity: critical
team: checkout
service: orders-api
annotations:
summary: 'orders-api 5xx ratio above 5% in {{ $labels.region }}'
runbook_url: 'https://runbooks.example.com/checkout/orders-api-5xx'
- alert: OrdersApiElevatedErrorRate
expr: |
sum by (service, region) (
rate(http_requests_total{service="orders-api", status=~"5.."}[10m])
)
/
sum by (service, region) (
rate(http_requests_total{service="orders-api"}[10m])
)
> 0.01
for: 15m
labels:
severity: warning
team: checkout
service: orders-api
annotations:
summary: 'orders-api 5xx ratio above 1% for 15 minutes'
runbook_url: 'https://runbooks.example.com/checkout/orders-api-warn'
Two rules, two severities. The first pages at 5 minutes; the
second files a ticket at 15. Both belong to the checkout rota
via the team label.
The Alertmanager side. The corresponding alertmanager.yml
fragment:
route:
receiver: default-receiver
group_by: [alertname, region]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity = "critical"
- team = "checkout"
receiver: pager-checkout-primary
continue: false
- matchers:
- severity = "critical"
- team = "platform"
receiver: pager-platform-primary
continue: false
- matchers:
- severity = "warning"
receiver: jira-tickets
continue: false
- matchers:
- severity = "info"
receiver: slack-low-priority
continue: false
receivers:
- name: default-receiver
email_configs:
- to: '[email protected]'
- name: pager-checkout-primary
pagerduty_configs:
- service_key: '<redacted>'
- name: pager-platform-primary
pagerduty_configs:
- service_key: '<redacted>'
- name: jira-tickets
webhook_configs:
- url: 'https://jira.example.com/webhook/alertmanager'
- name: slack-low-priority
slack_configs:
- api_url: '<redacted>'
channel: '#alerts-low'
Three things to notice. First, the route order matters: more
specific routes go first. Second, continue: false (the default)
stops the walk after the first match, which is why the
severity=warning route does not also fall through to the
catch-all. Third, every receiver has a backstop: email for the
catch-all, PagerDuty for paging, a webhook for tickets, Slack for
chat.
How to validate it
Three checks.
# 1. Is the route tree syntactically valid?
amtool check-config alertmanager.yml
Expected output on a clean file:
Checking 'alertmanager.yml'
SUCCESS
# 2. Does the route actually match a real alert? Use amtool to render.
amtool config routes test --config.file=alertmanager.yml \
--alertmanager.url=http://alertmanager:9093 \
severity=critical team=checkout service=orders-api
Expected output:
receiver: pager-checkout-primary
If the response says default-receiver, the route tree does not
match the labels you expected. Fix the matcher spelling, the label
spelling, or the route order.
# 3. End-to-end: list the live alerts Alertmanager is currently holding.
curl -s http://alertmanager:9093/api/v2/alerts \
| jq '.[] | {name: .labels.alertname, severity: .labels.severity,
team: .labels.team, receiver: .receivers[].name}'
Expected output during an incident:
{
"name": "OrdersApiHighErrorRate",
"severity": "critical",
"team": "checkout",
"receiver": "pager-checkout-primary"
}
The receivers field confirms the match in production.
How it can fail
Six failure modes:
-
Spelling drift.
severity: Criticalon the rule,severity = "critical"on the route. The route does not match. Symptom: critical alerts fall through to the catch-all, which is usually email. Confirm byamtool config routes testwith the live alert labels. -
Route order traps. A broad matcher earlier in the tree shadows a specific matcher later. Symptom: critical alerts meant for
team=checkoutland inteam=platform’s pager. Reorder the routes so the most specific is first. -
Single severity scheme. Every rule is
severity: criticalbecause that is what gets paged. Symptom: ticket-shaped alerts wake the on-call; the rota burns out; pages start getting snoozed. Addwarningandinfo, retune the route tree. -
continue: truecascade. A route withcontinue: truematches an alert that should have stopped one level higher. Symptom: the same alert appears in three receivers. Confirm by listingreceiversfor a live alert. -
Missing
teamlabel. A rule omitsteam:and lands in the catch-all. Symptom: nobody owns the alert; nobody acts on it. Maketeammandatory in the rule-review checklist. -
Public-vs-private confusion. A rule marked
severity: criticalbut tied to a status page incident gets routed as a page but never reaches the status-page receiver. Symptom: the customer sees no incident while the rota pages. Separate the internalseveritylabel from apublic_severityannotation that the status-page integration reads.
How to troubleshoot it
In order:
- What labels reached Alertmanager?
curl http://alertmanager:9093/ api/v2/alerts | jq '.[] | .labels'. Confirmseverity,team,service,alertnameare present and lower-case. - Which receiver did the dispatcher pick? Same endpoint, look
at
receivers[]. If it isdefault-receiver, the matcher chain failed somewhere. - Would the matcher pick the right receiver in isolation? Run
amtool config routes testwith the same labels. If the answer differs from what production did, the route tree in production is stale; reload Alertmanager withSIGHUP. - Did the receiver try to notify? Tail Alertmanager logs for
the fingerprint;
notifylog lines show webhook attempts and their HTTP status codes. - Did the receiver succeed? A 200 from the webhook may still be a logical failure (e.g. PagerDuty returns 200 but reports the service key is invalid). Check the receiver-specific integration dashboard.
Security implications
Routing labels are not sensitive on their own, but the receivers they point at are. A misconfigured route that sends a critical alert to a public Slack channel leaks the existence and nature of the incident. Treat the receiver list as part of the platform access control surface: who can read what, who can mute what, who can ack what.
The webhook URLs in receivers frequently carry API tokens. Alertmanager
supports the *_file variants (webhook_config_file,
pagerduty_config_file) so the secret never lives in
alertmanager.yml. Use them.
Performance implications
The route tree is walked on every alert dispatch. A tree with hundreds of routes and complex regex matchers can dominate the dispatcher. Common mitigations:
- Anchor regex matchers (
=~ "^checkout$"rather than=~ "checkout"). - Prefer equality (
=) over regex (=~). - Keep the tree small; collapse narrow routes into a single matchers block where possible.
Grouping (group_by: [alertname, region]) reduces the number of
notifications the receiver has to handle, but does not reduce the
dispatcher CPU cost.
Production guidance
- Pick the label set once:
severity,team,service, optionallyenv. Document it. Review rules against it. - Three severity values cover almost every estate:
info,warning,critical. Resist a fourth unless the business really demands it; the cognitive load of four values is heavier than the precision it adds. - Make
severitydrive the receiver, not the page-vs-ticket decision in isolation. Acriticalalert still goes to a ticket queue if the team rota is offline and the receiver has a fallback. - Separate internal severity from public severity. The label
severityis for the on-call rota. The annotationpublic_severity(or a label likecustomer_impact) feeds the status page integration.
Verification
- What three labels should a production alert estate carry on every rule?
- How does an Alertmanager route match an alert, and what is the
effect of
continue: true? - Why does a single-severity estate tend to fail as the rota grows?
- How does internal severity differ from public severity, and where does each live?
Quiz
Knowledge check · 8 questions
Q1. In an Alertmanager route tree, the matchers block on a route matches alerts based on:
Q2. A single severity scheme where every rule is marked critical tends to fail operationally when:
Q3. A critical severity alert must always page; a warning severity alert must never page. This binary is the canonical Prometheus convention.
Q4. The label most commonly used to drive ownership in Alertmanager routing is:
Q5. Name two label keys that should be set on every Prometheus alert rule to drive Alertmanager routing.
Q6. Which of these are reasonable labels to add to an alert rule for routing purposes?
Q7. A public severity label (for example customer_impact) is typically used to:
Q8. When the same incident produces both a warning and a critical alert, the cleanest routing outcome is usually achieved by:
Passing score: 75%. Answers are checked in this browser.