Annotation Templates
Alert annotations and labels support Go template strings, so the text an alert sends can describe what actually happened rather than repeating a fixed sentence. You can pull in the value that fired the rule, the labels on the series, and the threshold it crossed - and change the wording depending on any of them.
Templates are written in Go's text/template syntax, the same syntax Prometheus and Grafana use.
For advanced users
Plain text annotations work perfectly well. Templates are worth reaching for when you want an alert to explain itself - which host, which value, and how far past the limit.
Where templates work
Templates can be used in any annotation on an alert rule - Summary, Description, Runbook URL, and any custom annotation you add with + Add annotation - and in custom labels. See Rules for where these sit in the rule editor.
Variables
| Variable | Contains |
|---|---|
$labels |
The labels from the query, such as {{ $labels.instance }} |
$values |
The labels and values of every instant query and expression, indexed by reference ID - {{ $values.query.Value }} for the value, {{ $values.query.Labels }} for its labels |
$value |
A single string containing the labels and values of all instant queries, threshold, reduce, and math expressions. Useful for a quick dump, but $values is better when you want one specific number |
$values is indexed by the reference ID of each step in the query chain, so a rule built from the default Query → Reduce → Threshold chain exposes $values.query, $values.reduce, and $values.threshold. If you renamed a step, use the name you gave it.
A simple example
Error rate is {{ if $values.query }}{{ $values.query }}{{ else }}returning No Data{{ end }}% on {{ if $labels.instance }}{{ $labels.instance }}{{ else }}an unknown instance{{ end }}
The if/else pairs matter: an alert can fire when a query returns nothing, and a template that assumes a value is present will produce a confusing message when there isn't one.
Functions
Templates can transform values as well as print them.
| Function | Does |
|---|---|
humanize |
Abbreviates a decimal number - 1000.0 becomes 1k |
humanize1024 |
The same, base 1024 - 1024.0 becomes 1ki |
humanizeDuration |
Seconds to a readable duration - 60.0 becomes 1m 0s |
humanizePercentage |
A 0-1 ratio to a percentage - 0.2 becomes 20% |
humanizeTimestamp |
A Unix timestamp to a readable time |
title, toUpper, toLower |
Change the case of a string |
stripPort |
The hostname from a host:port string |
stripDomain |
Removes the domain, keeping the port |
match |
Tests a string against a regular expression |
reReplaceAll |
Replaces regular expression matches |
parseDuration |
A duration string such as 1h to seconds |
graphLink, tableLink |
A link to the graph or table view in Explore |
A worked example
This template reports CPU differently depending on whether the host is above or below its ceiling, names the host when it can, and says so plainly when the query returned nothing:
{{ if $values.query }}{{ $cpu := $values.query.Value }}{{ $host := or $labels.instance "an unidentified host" }}{{ if gt $cpu 80.0 }}{{ $host }} is running hot: CPU sustained at {{ humanize $cpu }}% over the last 5 minutes, past the 80% ceiling.{{ else }}{{ $host }} has settled to {{ humanize $cpu }}% CPU, back inside the 80% ceiling.{{ end }}{{ else }}CPU could not be read for {{ or $labels.instance "this host" }} — the query returned no value.{{ end }}
Keep it on one line
Go templates print the whitespace between actions, so breaking a template across indented lines puts those line breaks and spaces into the message. Either keep it on a single line, as above, or trim the whitespace with {{- and -}}.
What it produces:
| Case | Rendered message |
|---|---|
87.42 |
web-01 is running hot: CPU sustained at 87.42% over the last 5 minutes, past the 80% ceiling. |
41.66 |
web-01 has settled to 41.66% CPU, back inside the 80% ceiling. |
No instance label |
an unidentified host is running hot: CPU sustained at 88.1% over the last 5 minutes, past the 80% ceiling. |
| No data | CPU could not be read for this host — the query returned no value. |
Three things are doing the work:
$cpu := $values.query.Valueassigns the value once, so the rest of the template reads cleanlyor $labels.instance "an unidentified host"falls back when the label is missing, instead of printing nothinggt $cpu 80.0branches on the value, so the recovery message reads naturally rather than being a firing message with different numbers
Tip
Write the no-data branch first. It is the case most likely to reach someone at 3am, and the one most often forgotten.
More examples
Patterns worth adapting. Each assumes a query with the reference ID query - change that to match your own rule.
Bytes into something readable
humanize1024 turns a raw byte count into the units people actually use.
Disk on web-01 is down to 1.4GiB free.
A ratio as a percentage
If your query returns a ratio between 0 and 1, let humanizePercentage do the conversion rather than multiplying in PromQL.
Error ratio on checkout is 4.2%.
Seconds as a duration
payments has been degraded for 1h 12m 0s.
Dropping the port from an instance label
Prometheus targets usually carry host:port. stripPort gives you just the host.
web-01 is not responding to scrapes.
A runbook URL built from labels
Annotations that hold URLs can be templated too, so one rule can point at the right runbook per service.
Wording that follows severity
{{ if eq $labels.severity "critical" }}Page the on-call now: {{ else }}For review in the morning: {{ end }}{{ $labels.alertname }} on {{ $labels.instance }}.
Page the on-call now: HighLatency on web-01.
Showing every value in the chain
Useful while you are building a rule and want to see what each step produced. $value prints the labels and values of every instant query and expression in one go:
Annotation templates and notification templates
These are two different things, and they have different variables available:
| Written on | Variables | |
|---|---|---|
| Annotation templates | An alert rule, in its annotations and labels | $labels, $values, $value - the labels and values of this alert instance |
| Notification templates | A contact point, to shape the message it sends | .Alerts, .CommonLabels, and the fields of each alert - so one message can cover several alerts |
A rule that produces several series creates a separate alert instance per series, each with its own $labels and $values. Iterating over a group of alerts belongs in a notification template, not here. See Contact Points for those.
Further reading
- Go text/template - the template syntax itself
- Annotation and label template reference - the full list of variables and functions
- Prometheus notification templates - the values available in notifications
Need more help?
Contact support in the chat bubble and let us know how we can assist.