nanook-templates

Render alert bodies, webhook payloads, exec args.

nanook-templates is the small templating language used wherever nanook splices runtime data into a string: alert bodies, webhook URLs, exec command lines. Jinja-ish syntax, parses at config load so typos surface during nanook check.

The four sites

A template string is a mix of:

  1. Literals · anything outside {{ }} and {% %}.
  2. Substitutions · {{ expr }} evaluates and inserts the result.
  3. Conditionals · {% if expr %}then{% else %}otherwise{% endif %}.
  4. Loops · {% for x in iter %}body{% endfor %}.

To emit a literal {{, escape it as \{{.

Quick taste

{{ rule }} fired with {{ trigger.val | round(1) }} on {{ trigger.source }}
{% if trigger.labels.core %}core {{ trigger.labels.core }} pinned hot{% endif %}

Substitutions

{{ expr }}. Whitespace inside is trimmed.

Paths

Dotted field access and bracket key lookup. The render context for an alert template is the full AlertEvent:

PathWhat
kindfire, resolve, or escalate
ruleThe rule's expression as a string
channelThe destination channel id
messageThe default rendered alert message
atRFC3339 timestamp of the dispatch
trigger.nameThe metric name (e.g. cpu.usage)
trigger.valThe value that tripped the rule
trigger.sourceThe collector that emitted it
trigger.labels.<key>Label value by name (e.g. trigger.labels.core)
trigger.timestampRFC3339 timestamp of the sample
trigger.idStable id for the metric series

trigger is absent on window-recheck rules where no specific sample drove the transition. Guard with or if you reference it from a rule that may fire that way.

{{ trigger.val }}                  # 92.4
{{ trigger.labels.mount }}         # /var
{{ trigger.labels[disk] }}         # bracket form, same thing
{{ trigger.source }}               # cpu

Literals

{{ "literal string" }}
{{ 42 }}
{{ true }}

or fallback

{{ left or right }} evaluates left. If it errors or comes back empty, it evaluates right instead. Chain left-to-right.

{{ trigger.labels.region or "unknown" }}
{{ trigger.labels.region or trigger.labels.zone or "?" }}

Filters

Pipe a value through a transform. Filters chain.

{{ trigger.val | round(2) }}
{{ trigger.name | upper }}
{{ rule | trim | truncate(80) }}

Built-in filters:

FilterArgsWhat
upperuppercase
lowerlowercase
trimstrip leading/trailing whitespace
lenlength of string, list, or map
default(x)1replace empty input with x
truncate(n)1clip to N chars, append if clipped
round(n)1round float to N decimals
floorfloor a number
ceilceil a number
urlencodepercent-encode for URL query strings
shPOSIX single-quote escape for shell command lines
jsonJSON-encode the value (strings get quoted+escaped)

sh and json exist because templating values into shell command lines or JSON bodies is otherwise a quoting hazard. Reach for them whenever a value crosses into one of those layers:

cmd = "mail -s {{ trigger.labels.host | sh }} alice@example.com"
body = '{"text": {{ message | json }}, "host": {{ trigger.labels.host | json }}}'

sh always wraps in single quotes (so the shell sees one argument no matter what's inside). json produces valid JSON for any value: strings come out quoted and escaped, numbers and bools pass through, lists become arrays, maps become objects, missing fields become null.

Conditionals

{% if trigger.labels.severity == "critical" %}
  PAGE THE ONCALL
{% else %}
  log it and move on
{% endif %}

Truthiness is loose on purpose. Falsy values: "", "false", "0", plus any expression that errors during render. Everything else is truthy.

{% else %} is optional. There is no {% elif %}. Nest {% if %} blocks if you need branching.

Loops

mounts:
{% for m in mounts %}
  - {{ m.path }}: {{ m.usage }}%
{% endfor %}

Iterates over a list, set, or map's values. The bound name (m here) shadows any outer field of the same name inside the loop body.

Fall-through behaviour

A path that doesn't resolve raises a render error. The owning handler decides what to do: webhook channels fall back to the default JSON body, exec channels fall back to the raw template source, log channels fall back to the default message. Use or and default to make templates robust.

# robust
{{ trigger.labels.region or "?" }}

# fragile, errors if trigger.labels.region is missing
{{ trigger.labels.region }}

Where templates appear

FieldTemplate?
[channels.<id>.opts].url (webhook, discord, slack)yes (you can interpolate secrets here)
[channels.<id>.opts].body (webhook, discord, slack)yes
[channels.<id>.opts].cmd (exec channel)yes
[[alerts]].bodyyes, rendered by every action with a fallback to the default message on render error

When in doubt, run nanook check.

Escaping

\{{ this is a literal pair of braces }}

There is no escape for {% because it's vanishingly rare in plain prose. If you really need a literal {%, split it across the literal: { %.

See also

  • Channels · channel opts that take templates
  • Alerts · how the AlertEvent context is built