Migration Guide
This guide covers the breaking and operationally visible changes between releases, newest first.
Upgrading to 2.1.0
Before you upgrade
-
Validate the production configuration with the new binary, with the environment variables of the notifiers defined (dummy values are fine), and fix every reported error: validation is stricter (see Stricter configuration validation).
sudo MATTERMOST_WEBHOOK="https://mattermost.example.com/hooks/dummy" \ ./valerter --validate -c /etc/valerter/config.yaml - Keep your configuration when dpkg asks.
/etc/valerter/config.yamlis a conffile: dpkg asks what to do with your modified version. AnswerN(keep it), or upgrade non-interactively with--force-confold; answeringYreplaces it with the example configuration. See Upgrading the .deb package. - Expect the upgrade to block for up to ~20 s: the restart now waits for the queued alerts to be delivered (see Queued alerts are delivered on shutdown). Containers need a 30 s stop timeout.
- Review your PromQL (see Prometheus metrics: label sets and counting and One notification queue per destination).
The sections below detail every change visible to an operator.
A configuration with all rules disabled is now refused
Until 2.0.3, a configuration whose rules were all enabled: false passed validation; the daemon then started, found nothing to watch and exited with code 0. It is now a validation error, reported by valerter --validate and at startup (exit code 1):
all rules are disabled: enable at least one rule in config.yaml or rules.d/
To pause alerting, stop and disable the service rather than disabling every rule:
sudo systemctl disable --now valerter
Run valerter --validate -c /etc/valerter/config.yaml before upgrading to catch this case.
Exit codes and systemd restarts
- Exit code 0 now only follows a requested shutdown (SIGINT/SIGTERM). systemd does not restart the unit.
- Exit code 1 covers every failure: invalid configuration (as before), all rules disabled (new), and the engine losing all its tasks at runtime (new, previously exit code 0).
- Under the shipped unit (
Restart=on-failure,RestartSec=5), any exit code 1 makes systemd restart valerter every 5 seconds for as long as the cause persists. This restart loop already existed for invalid configurations; it now also applies to a configuration with all rules disabled. A dedicated exit code for configuration errors (to stop the loop withRestartPreventExitStatus) is out of scope for this release. - You can now alert on the unit state (
failed, or repeated restarts) or on the process exit code: a valerter that stops watching no longer looks like a clean stop.
Automatic restart on .deb upgrade
dpkg -i of a new version now restarts the service when it is active or enabled; the manual systemctl restart valerter is no longer needed. If the service is not active two seconds after the restart (typically an invalid configuration), a warning pointing to journalctl -u valerter is printed and the upgrade still succeeds.
- An enabled service that you stopped on purpose is started again by the upgrade. Disable it (
systemctl disable valerter) if it must stay stopped. - When upgrading from 2.0.3 or earlier, the old package stops the service before the new one is installed. An enabled service is restarted; a service started by hand without being enabled stays stopped: run
sudo systemctl start valerterafter the upgrade. - Containers and chroots. The maintainer scripts now touch the service only when systemd is the running init (
/run/systemd/systemexists). Installing or upgrading the package in a container or a chroot runs nosystemctlcommand and no longer printsvalerter failed to start after upgrade; start valerter the way your image does. - Removal.
dpkg -r valerternow stops the service whatever its state (includingactivating (auto-restart)between two restarts) and disables it before the package files are removed. Configuration and thevalerteruser are kept, as before.
VictoriaLogs streaming: logs, headers and discarded-lines metric
- Log field
delay_secsrenameddelay_ms. TheConnection failed, retryingwarning now carries the backoff delay in milliseconds (delay_ms=930) instead of whole seconds (delay_secs=0). Update any log query, alert or dashboard that filters ondelay_secs. The cause of the failure (Connection failed/Stream read error) is now logged before this warning instead of after the wait. - A custom
Authorizationheader now masksbasic_auth. When a source defines bothbasic_authandheaders: { Authorization: ... }, only the custom header is sent (previously both were sent and the server chose). A warning is logged at startup for each affected (rule, source) task. Keep only the credentials you actually want to use. Custom headers with the name of a default header (Accept,Connection) also replace it instead of being sent twice. valerter_lines_discarded_total{reason="invalid_utf8"}counts lines, not batches. Only the invalid line is dropped now, and each one adds one unit, so the counter may grow faster than in 2.0.3 for the same stream. Revisit any alert threshold set on it.
--validate is stricter
Until 2.0.3, valerter --validate stopped after loading and validating the configuration: a configuration with an unknown rule destination, an undefined notifier variable, a missing body_template_file or an email template without email_body_html passed it with exit code 0 and then failed at startup. --validate now runs every blocking startup check (same code, same messages, exit code 1), still without starting the daemon or making any network call.
-
Define every environment variable referenced by notifiers when running
--validate, including in CI pipelines that validated without secrets. Dummy values are fine, nothing is sent:MATTERMOST_WEBHOOK="https://mattermost.example.com/hooks/dummy" \ SMTP_PASSWORD="dummy" \ valerter --validate -c config.yamlVariables used by VictoriaLogs sources were already required by
--validate. body_template_filefiles must be readable by the user running--validate, not only by thevalerterservice user.- New errors you may now get from
--validate:Notifier configuration error(Failed to create notifiers: N errors),Destination validation error(Destination validation failed: N errors) andEmail template validation error(Email template validation failed: N errors). All of them are reported in one run, and the daemon now does the same at startup instead of stopping at the first failed stage. - Summary format. A ` Notifiers:
[ = , ...]` line is printed after `Templates`, and source URLs are redacted (`http://***@vl.example.com:9428/?***` for a URL carrying credentials or a query string; URLs without such parts are unchanged). Update scripts that parse this output.
Stricter configuration validation
valerter 2.1.0 refuses, when the configuration is loaded or when notifiers are built, several mistakes that 2.0.3 accepted and that only showed up at runtime (silently suppressed alerts, a single throttle counter, every send failing, an endless reconnection loop). The upgraded service refuses to start if your configuration has one of them. Before installing the new package, validate the production configuration with the new binary, with the environment variables of the notifiers defined (dummy values are fine), and fix every reported error:
valerter --validate -c /etc/valerter/config.yaml
| 2.0.3 behavior | 2.1.0 error | Fix |
|---|---|---|
defaults.throttle.count: 0 accepted with a runtime warning: every rule without its own throttle sent at most its first alert |
defaults.throttle.count must be >= 1 (0 would suppress every alert) |
Set count to 1 or more |
defaults.throttle.window: 0s accepted with a runtime warning: throttling silently disabled |
defaults.throttle.window must be > 0 (0s disables throttling) |
Set a positive window (e.g. 60s) |
The same notifier listed twice in a rule’s notify.destinations: every alert delivered twice to it |
rule '<rule>': notify.destinations contains duplicate entry '<name>' (each notifier may appear at most once) (also for disabled rules) |
Remove the duplicate |
Unknown filter, test or function in throttle.key (rule or defaults): every event of the rule fell back to the single key <rule>:error |
invalid template in rule '<rule>': throttle.key render: ... / defaults.throttle.key render: ... |
Fix or remove the filter; syntax errors in defaults.throttle.key are now reported too (defaults.throttle.key: ...) |
Unknown filter, test or function in a webhook, Telegram or email body_template (inline, body_template_file or default): every send failed |
Notifier configuration error with invalid notifier '<name>': body_template render: ... |
Fix the template; the error names the filter |
Invalid header name (space, :…) or value (line break) in victorialogs.<source>.headers: endless reconnection loop |
victorialogs.<source>.headers: invalid header name '<name>' / invalid value for header '<name>' (the value is never printed) |
Fix the header name, or the value or variable it resolves to |
Telegram parse_mode other than HTML, MarkdownV2 or Markdown (e.g. Markdown2, markdown_v2): HTTP 400 for every alert |
invalid notifier '<name>': parse_mode '<value>' is not supported (expected HTML, MarkdownV2 or Markdown) |
Use one of the three values; the case no longer matters (html is sent as HTML) |
VictoriaLogs source url resolving to a value still containing ${: accepted without any check |
victorialogs.<source>.url: invalid URL: ... |
Give the variable an http(s):// URL |
Webhook url or Mattermost webhook_url built from a ${VAR} that resolves to a wrong scheme or a non-URL: every send failed |
invalid notifier '<name>': url: invalid URL: ... / webhook_url: invalid URL: ... (the URL is never printed) |
Fix the variable’s value |
The test render of templates is also more accurate, in templates, throttle.key, subject_template and body_template:
- Accepted now: conversions and arithmetic on fields (
{{ status | int }},{{ (latency | float) > 1.5 }},{{ count + 1 }}) and a division between two plain identifiers ({{ total/count }}), wrongly refused until 2.0.3. - Refused now: an unknown filter, test or function placed after a built-in filter applied to a field (
{{ status | int }}-{{ host | truncat(10) }},{{ host | split('.') | first }} {{ host | nosuch }}), in anelsebranch, in{% if not x %}, underis not definedor in a{% for %}body. The test render runs twice, once with every condition true and every loop over a field iterating one element, once with every condition false, every loop empty andis definedfalse. Such templates failed at runtime (athrottle.keyfalling back to<rule>:error, sends failing); fix the reported filter. - Still not checked: the part of a template after arithmetic on a raw field (
{{ count + 1 }}stops the check of that pass; write{{ count | int + 1 }}, which also works at runtime since VictoriaLogs fields are strings) and the body of anelif. See docs/templates.md.
Custom throttle keys are now shared across a rule’s sources
Until 2.0.3, each (rule, source) task had its own throttle cache, so throttle.key: "{{ rule_name }}" did not dedup across sources as documented: the same outage seen by two sources sent two alerts. In 2.1.0 a rule has a single throttle cache shared by all its sources, and every source that renders the same key increments the same counter.
- Rules without
throttle.key(default key<rule_name>-<vl_source>:global) or whose key already contains{{ vl_source }}: no change, each source keeps its own counter. -
Rules targeting several sources with a custom key that does not contain
{{ vl_source }}(e.g.{{ rule_name }},{{ host }}, also when inherited fromdefaults.throttle.key): the counter is now shared by the rule’s sources, so these rules may send fewer alerts. To keep one counter per source, add{{ vl_source }}to the key:throttle: key: "{{ vl_source }}-{{ host }}" # was "{{ host }}" - Startup log. valerter logs at INFO level, once per affected rule,
Throttle key does not reference vl_source: its counter is shared across the rule's sources; add {{ vl_source }} to the key to isolate them, with therule_name,source_countandthrottle_keyfields. Search your logs for it after the upgrade to list the rules concerned. - Reconnection reset. When a source reconnects after an error, only the counters fed exclusively by that source are reset; counters shared with another source are kept. The throttle cache also survives a task restart after a panic.
- Cache bound. A rule’s throttle cache holds up to 10,000 keys per source it targets (previously 10,000 per
(rule, source)task): same total memory budget.
One notification queue per destination
Until 2.0.3, every alert went through a single queue consumed by a single worker, so one unresponsive notifier delayed all the others. In 2.1.0 each notifier has its own queue and its own delivery task. No configuration change is needed; the visible changes are:
- Order is guaranteed per destination. Alerts reach each notifier in the order they were produced, but there is no ordering between destinations anymore: a healthy channel can receive an alert before a slow one receives an older alert.
- Capacity is exactly 100 alerts per destination (the old single queue held 128). When a destination queue is full, its oldest alert is dropped, for that destination only. The
Queue full, dropping N oldest alertswarning now carries anotifierfield. valerter_queue_sizeandvalerter_alerts_dropped_totalcount deliveries summed over every destination. They keep their names and have no label, so existing selectors still work, but an alert pending (or dropped) for two destinations counts twice.- New series:
valerter_destination_queue_size{notifier_name, notifier_type}andvalerter_destination_alerts_dropped_total{notifier_name, notifier_type}, exposed at 0 for every notifier from startup. -
Review PromQL alerts with an absolute threshold on
valerter_queue_size. As a sum over destinations it can now reach 100 × number of notifiers, sovalerter_queue_size > 50no longer means “queue half full”: it can fire while no queue is half full, or stay silent while one destination overflows if the threshold was tuned for 128. Rewrite it on the per-destination gauge:# before expr: valerter_queue_size > 50 # after expr: max(valerter_destination_queue_size) > 50Alerts based on
rate(valerter_alerts_dropped_total[...])keep their meaning and need no change; usevalerter_destination_alerts_dropped_totalto see which destination drops.
Queued alerts are delivered on shutdown
Until 2.0.3, a systemctl stop or restart dropped every alert still in the queue. In 2.1.0, on SIGTERM/SIGINT the rule tasks stop first, then every destination delivers its in-flight and queued alerts before the process exits, within 20 seconds. No configuration change is needed; the visible changes are:
- A shutdown can take up to ~20 s (about 20 s more than before) when alerts are queued or an endpoint is slow. With empty queues the process still exits at once. If the 20 s expire, the WARN
Shutdown drain timeout reached, alerts not deliveredgives the number of alerts lost (undelivered) and the exit code stays 0. - Package upgrades can block
dpkg/aptfor up to ~20 s. Thesystemctl restartrun by the Debianpostinstwaits for the old process to drain its queues. This is expected: do not interrupt the upgrade. The shipped unit (TimeoutStopSec=30) needs no change. - Raise the stop timeout of container runtimes to 30 s to benefit from the drain:
docker stop --stop-timeout 30 valerter, orstop_grace_period: 30sin Docker Compose. Docker’s default of 10 s kills the process before the drain ends (alerts are then lost, as before 2.1.0). Use the same value for any other supervisor that sends SIGKILL after a delay. - A second SIGTERM/SIGINT forces an immediate exit (e.g. a second Ctrl+C), whatever the shutdown phase. It logs
Second shutdown signal received, forcing immediate exitand exits with code 1, since queued alerts may be lost.
Prometheus metrics: label sets and counting
2.1.0 makes the series exposed at startup match the ones that are incremented, and gives each counter a single meaning. No configuration change is needed; review your PromQL queries, dashboards and alerts:
-
Breaking: startup series with reduced labels are removed. Until 2.0.3, valerter seeded at startup
valerter_alerts_sent_total{rule_name, vl_source},valerter_parse_errors_total{rule_name, vl_source}(withouterror_type),valerter_alerts_failed_total{notifier}andvalerter_notify_errors_total{notifier}. Nothing ever incremented them: they stayed at 0 next to the real series, which only appeared at the first event. They are gone, and every series is now created at 0 with its emission labels (rule_name,vl_source,notifier_name,notifier_typefor the notification counters;error_typefor parse errors; see docs/metrics.md). Replace thenotifierlabel withnotifier_name:# before expr: increase(valerter_alerts_failed_total{notifier="mattermost-ops"}[1h]) > 0 # after expr: sum by (notifier_name) (increase(valerter_alerts_failed_total{notifier_name="mattermost-ops"}[1h])) > 0Unfiltered sums (
sum(valerter_alerts_sent_total)) keep their values, since the removed series were always 0. A notifier used by no enabled rule no longer hasvalerter_alerts_failed_totalorvalerter_notify_errors_totalseries: list notifiers withvalerter_destination_queue_size, which exists for every notifier. - Breaking:
valerter_notifier_config_errors_totalis removed. It was incremented before the metrics exporter started and the error stops valerter with exit code 1, so no scrape could ever see it. To detect a notifier configuration error (e.g. an undefined${VAR}), alert on the systemd unit state (failed, repeated restarts) or on the ERROR logNotifier configuration error, and runvalerter --validatebefore deploying. -
Breaking:
valerter_reconnections_totalno longer counts clean stream ends. 2.0.3 started counting a clean end of the stream (HTTP 200 then EOF without error) invalerter_reconnections_total, in a release announced without breaking changes. 2.1.0 reverts that choice:valerter_reconnections_totalonly counts reconnections after a failure (connection error, HTTP error response, error while reading the stream), and clean ends are counted in the newvalerter_stream_ends_total{rule_name, vl_source}. Dashboards and alerts built since 2.0.3 see lower values, especially behind a proxy or load balancer that closes idle streams. To get the 2.0.3 total back:valerter_reconnections_total + valerter_stream_ends_totalAn alert such as
rate(valerter_reconnections_total[5m]) > 0now only fires on failures, which makes it usable without filtering out normal stream ends. -
Breaking: Telegram alerts are counted once per alert. Until 2.0.3, a Telegram notifier incremented
valerter_alerts_sent_totalonce per successful chat andvalerter_notify_errors_total/valerter_alerts_failed_totalonce per failed chat, even when the alert reached other chats. It now counts like the email notifier:valerter_alerts_sent_total+1 when at least one chat received the alert,valerter_notify_errors_totalandvalerter_alerts_failed_total+1 only when every chat failed. For a notifier with severalchat_ids,valerter_alerts_sent_totalis therefore divided by the number of chats, andrate(valerter_alerts_failed_total[5m]) > 0no longer fires on a partial failure. To watch individual chats, use the newvalerter_telegram_chat_errors_total{rule_name, vl_source, notifier_name}(one unit per failed chat; a chat delivered through the plain-text fallback is not a failure):expr: increase(valerter_telegram_chat_errors_total[15m]) > 0 valerter_vl_source_uponly exists for sources targeted by an enabled rule. Until 2.0.3 every declared source got the gauge at 0, and a source no enabled rule tails stayed at 0 forever, firingvalerter_vl_source_up == 0alerts for a source nobody watches. Such a source now has no series;min(valerter_vl_source_up) == 0and per-source alerts keep working for the sources that are tailed.- Template render errors at send time are counted. A webhook
body_template, an email subject or body, or a Telegram message text that fails to render now incrementsvalerter_notify_errors_totalandvalerter_alerts_failed_total; it used to be only logged. Most of these errors are now refused at startup (see Stricter configuration validation).
${VAR} is resolved in webhook body_template
Breaking: until 2.0.3, a ${VAR} written in a webhook body_template was sent literally, while url and headers of the same notifier were resolved from the environment. In 2.1.0, ${VAR} placeholders of the template source are resolved too, once, when the notifier is built (at startup and by valerter --validate). An undefined variable refuses the configuration (exit code 1):
invalid notifier 'pagerduty': body_template: invalid configuration: undefined environment variable: PAGERDUTY_ROUTING_KEY
- Find the affected templates: search for
${in thebody_templateof your webhook notifiers (config.yamlandnotifiers.d/*.yaml). For each match, either define the variable in the service environment (and when running--validate), or remove the placeholder. To keep a literal${...}, write{{ '$' }}{VAR}: the substitution does not recognize it and the render produces${VAR}. - Values rendered from logs are never resolved. A
${HOME}inside a log line or an event field inserted with{{ body }}is sent as is. - The value is inserted before Jinja parses the template. A value containing
{{,{%or"is interpreted by Jinja or breaks the JSON body; keep such values in a header instead. The resolved template is never logged, but a syntax error caused by such a value may quote a fragment of it in the error message. - Substitution is a single pass, in
body_templateas in every other resolved field: a variable whose value contains${...}is inserted as is, never resolved again. - Telegram and email
body_templateare unchanged (no${VAR}resolution).
The PagerDuty example now reads its routing key from the environment instead of storing it in the configuration file:
# before (2.0.3: "${PAGERDUTY_ROUTING_KEY}" was sent literally)
pagerduty:
type: webhook
url: "https://events.pagerduty.com/v2/enqueue"
headers:
Authorization: "Token token=${PAGERDUTY_TOKEN}"
body_template: |
{"routing_key": "${PAGERDUTY_ROUTING_KEY}", "event_action": "trigger",
"payload": {"summary": "{{ title }}", "source": "valerter", "severity": "error"}}
# after (2.1.0: set PAGERDUTY_ROUTING_KEY in the service environment)
pagerduty:
type: webhook
url: "https://events.pagerduty.com/v2/enqueue"
body_template: |
{"routing_key": "${PAGERDUTY_ROUTING_KEY}", "event_action": "trigger",
"payload": {"summary": {{ title | tojson }}, "source": "valerter", "severity": "error"}}
With the shipped systemd unit, define the variable like your other notifier secrets, e.g. in a drop-in (sudo systemctl edit valerter, then Environment=PAGERDUTY_ROUTING_KEY=... under [Service]).
mattermost_channel is now applied
Breaking: until 2.0.3, a rule’s notify.mattermost_channel was documented as a channel override but silently ignored: alerts went to the notifier’s channel, or to the webhook’s default channel. In 2.1.0 it is applied, so alerts of a rule that sets it now go to that channel. The channel is chosen in this order: the rule’s mattermost_channel, then the notifier’s channel, then the webhook’s default channel.
- To keep the previous behavior, remove
mattermost_channelfrom the rule. Search for it inconfig.yamlandrules.d/*.yaml. -
Check the channel and the webhook. The key was never exercised, so a misspelled channel or a webhook created with “Lock to this channel” is likely. When Mattermost rejects the rule’s channel with a 4xx response other than 429, the alert is resent once to the notifier’s
channelif it has one (and it differs from the rule’s), otherwise withoutchannel(to the webhook’s default channel): in both cases where the alert went before the upgrade. The WARN below is logged for every such alert, until you fix the channel name, unlock the webhook or remove the key (fallback_channelis absent when the resend has nochannel):Mattermost rejected channel override, resending to notifier default notifier_name=<notifier> rule_name=<rule> channel=<requested channel> fallback_channel=<notifier channel> status=<status>The resend costs one extra request per alert; the alert is counted once in the metrics, as sent or failed. A 4xx on the resend fails the alert. The notifier’s own
channelkeeps its behavior: a 4xx fails the alert immediately. - Rules without a Mattermost destination still log
mattermost_channel ignored - no mattermost notifier in destinationsat startup; other notifier types ignore the key.
New features (no action required)
Existing templates render as before. 2.1.0 adds:
- Markdown bodies. With
body_format: markdown, thebodyof a rule template is written once in Markdown and rendered for each notifier: Markdown for Mattermost, HTML for Telegram and email (noemail_body_htmlneeded), plain text for a webhook by default. Values of the log are escaped automatically. See Markdown bodies. formatper notifier, to choose the rendering of a Markdown body (plain,markdownorhtmlfor a webhook,telegram_htmlorplainfor Telegram). See Output format per notifier.- The
logvariable in notifier templates (webhook, Telegram and emailbody_template, emailsubject_template): every field of the event,{{ log.host }},{{ log["k8s.pod"] }}or{{ log.k8s.pod }},{{ log | tojson }}for the whole event. Rule templates (title,body,email_body_html,throttle.key) are unchanged and keep reading fields at the top level ({{ host }}); an event field namedlogstays available there as{{ log }}. - Filters and functions:
md_escapeandmdv2_escape(escape a value for Mattermost Markdown and Telegram MarkdownV2, next to| efor HTML),code,codeblockandmd_link(build Markdown from values), and minijinja’surlencode. See valerter filters and functions. - A possible new warning at startup and in
--validate:Notifier template references unknown variable notifier=<name> field=<body_template|subject_template> variable=<name>. Such a variable already rendered empty; the warning points at it, usually a log field written{{ host }}instead of{{ log.host }}. Fix the template, or remove the variable, to silence it. The exit code is unchanged.
Telegram: move the markup from body to body_template. Until now the documentation advised writing Telegram HTML tags in the rule template’s body. With the default Telegram body_template (<b>{{ title|e }}</b>\n{{ body|e }}), body is escaped and those tags were shown literally; a custom {{ body }} without |e displayed them, but then a < or & from the log line broke the message. Write the markup in the Telegram body_template and escape each value there:
# before: markup in the rule template, shown literally by Telegram
templates:
disk_alert:
title: "Disk alert"
body: "Host: <code>{{ host }}</code>"
email_body_html: "<p>Host: {{ host }}</p>"
notifiers:
telegram-ops:
type: telegram
bot_token: "${TELEGRAM_BOT_TOKEN}"
chat_ids: ["-100123456789"]
# after: plain rule template, markup in the Telegram body_template
templates:
disk_alert:
title: "Disk alert"
body: "Host: {{ host }}"
email_body_html: "<p>Host: {{ host }}</p>"
notifiers:
telegram-ops:
type: telegram
bot_token: "${TELEGRAM_BOT_TOKEN}"
chat_ids: ["-100123456789"]
body_template: |
<b>{{ title|e }}</b>
Host: <code>{{ log.host|e }}</code>
The other notifiers of the rule keep receiving the plain body. See docs/notifiers.md. A Markdown body is the other option: written once, it reaches Telegram as HTML.
Other behavior changes
No configuration change is needed for these, but they may change what you see:
- Webhook requests carry
Content-Type: application/jsonunlessheadersdefines aContent-Type. Set one explicitly if your endpoint expects another type. A body that is not valid JSON while theContent-Typeis JSON now logs the WARNWebhook body is not valid JSON(the request is still sent): insert values with| tojson. - SMTP
5xxreplies are no longer retried (503,530,541… were retried three times);4xxreplies and network, TLS or timeout errors are retried. The decision depends on the reply code only. - Fatal errors are logged once, at ERROR, in the configured log format: the extra unstructured
Error: ...line on stderr is gone. Update anything that parsed it. - Text logs have no ANSI color codes in journald or a file (colors only on a terminal).
- A task that panics is restarted after 5 s, doubled at each consecutive panic up to 5 min (instead of every 5 s forever); it is never abandoned. Alert on
valerter_rule_panics_total. - Rules of
rules.d/keep a stable order: the rules ofconfig.yamlin their declared order, then therules.d/rules sorted by file path, then by rule name within a file. This shows in the logs and the--validatesummary. - Telegram: a message rejected with
can't parse entitiesin HTML mode is resent once as plain text instead of being lost (WARNTelegram rejected HTML message, resending as plain text), and the truncation to 4096 characters counts only the visible text and never cuts a tag or an entity. See Telegram. - Email: the plain
bodyinserted in an email body template (the message sent when a rule template fails to render) is now escaped as HTML.
Rolling back to 2.0.3
There is no on-disk state: install the 2.0.3 package (or binary) again. Before that, undo the configuration changes 2.0.3 does not know, or it refuses to start:
body_formatin a template andformatin a notifier are unknown fields for 2.0.3 (configuration refused at load time);- the
md_escape,mdv2_escape,code,codeblockfilters, themd_linkfunction and theurlencodefilter are unknown to 2.0.3 (templates refused); logrenders empty in the notifier templates of 2.0.3, and a${VAR}in a webhookbody_templateis sent literally;- a rule’s
mattermost_channelis ignored again.
Prometheus queries rewritten for 2.1.0 (notifier_name, valerter_destination_queue_size, valerter_stream_ends_total, valerter_telegram_chat_errors_total) find no series under 2.0.3.
Upgrading from v1.x to v2.0.0
This section covers upgrading from Valerter v1.x to v2.0.0. Follow it section by section. Every breaking change has a before / after snippet you can copy.
If you only need a one-line summary: the victorialogs section is now a map of named sources, every per-rule Prometheus metric gained a vl_source label, and valerter_victorialogs_up was renamed.
A typical single-source v1.x configuration needs two edits: move the victorialogs settings under a source named default (see Config Migration), and, when coming from a release before 1.2.0, rename the template field body_html to email_body_html. valerter --validate names every place to change. Then follow Upgrading to 2.1.0, which applies on top.
1. Pre-Upgrade Checklist
Walk through this before you flip the binary. Each bullet is a concrete file or query you should touch.
Configuration
- Open every
config.yaml,rules.d/*.yaml, andnotifiers.d/*.yamlyou ship. - Find the top-level
victorialogs:block. If it has a directurl:key, you must rewrite it (see Section 2). - Decide on your source name(s). Names must match
^[a-zA-Z0-9_]+$. If you have only one backend today, pickdefault. - Decide if any rule should pin to a subset of sources via
vl_sources: [name, ...]. By default, rules fan out across every configured source.
Prometheus Dashboards
- Search every Grafana dashboard JSON for
valerter_victorialogs_up. Every match must move tovalerter_vl_source_up(see Section 3). - Search for PromQL queries that group by
rule_nameonly (e.g.sum by (rule_name) (valerter_alerts_sent_total)). They keep working but now silently aggregate across sources. If that is not what you want, addvl_sourceto thebyclause. - Verify panels on per-source overlays will not collapse if a rule fans out to multiple sources.
Prometheus Alerts
- Find every
alert:rule that referencedvalerter_victorialogs_up{rule_name=...}. Migrate the label key fromrule_nametovl_source(see Section 3 for examples). - Re-evaluate cardinality. Per-rule alerts now multiply by the number of sources you tail.
Notifier Output
- If you parse the Mattermost footer string downstream, expect a 4-segment
valerter | <rule> | <source> | <timestamp>instead of the 3-segment v1.x format (see Section 4). - If you consume the default webhook payload, expect a new top-level
vl_sourcefield.
2. Config Migration
The victorialogs section is now a map of named sources. The v1.x single-URL shape is rejected at load with an actionable error.
Before (v1.x)
victorialogs:
url: "http://victorialogs:9428"
basic_auth:
username: "u"
password: "p"
After (v2.0.0)
victorialogs:
default:
url: "http://victorialogs:9428"
basic_auth:
username: "u"
password: "p"
The minimum-effort migration is one new key (default:) and one extra indent level. Credentials, TLS, and headers move under each source, self-contained.
Targeting sources per rule
Add vl_sources: [name, ...] to a rule to restrict it to a subset of sources. Omit the field to fan out across every configured source:
rules:
- name: "prod_only_alert"
query: '_stream:{app="api"} level:error'
vl_sources: [prod] # only the `prod` source
parser: {}
notify: { template: "default_alert", destinations: ["mattermost-ops"] }
- name: "all_envs_alert"
query: '_stream:{app="auth"} "login failed"'
# no vl_sources → fans out across every source
parser: {}
notify: { template: "default_alert", destinations: ["mattermost-ops"] }
See examples/multi-source/ for a complete reference with prod + staging.
Source name format
Source names must match ^[a-zA-Z0-9_]+$. No dashes, dots, colons, or spaces. The constraint avoids ambiguity in the default throttle key format below. Validation runs at load time.
Default throttle key change
The default throttle key changed from the literal string <rule_name>:global (v1.x) to <rule_name>-<vl_source>:global (v2.0.0). These angle-bracket placeholders are descriptive notation, not template syntax. Multi-source deployments get isolated throttle buckets per source automatically. If you want cross-source dedup (one bucket shared across sources for the same rule), set throttle.key explicitly:
rules:
- name: "shared_bucket_alert"
query: '_stream:{app="api"} level:error'
parser: {}
throttle:
key: "{{ rule_name }}" # back to v1.x semantics
count: 5
window: 60s
notify: { template: "default_alert", destinations: ["mattermost-ops"] }
Cross-source dedup with a custom key is effective since 2.1.0; in 2.0.x each source still counted separately. See Custom throttle keys are now shared across a rule’s sources.
defaults.max_streams cap
A new cap on total (rule, source) pairs spawned, default 50. Disabled rules do not contribute. Breaching the cap fails the config at load with both the actual count and the cap value. Tune via defaults.max_streams: <usize> if you fan out many rules across many sources.
3. Prometheus Migration
Removed: valerter_victorialogs_up{rule_name}
This per-rule gauge was replaced by a per-source gauge. Reachability is a property of the source, not the rule (every rule that tails the same backend reports the same up/down state).
Added: valerter_vl_source_up{vl_source}
One value per source, regardless of how many rules tail it: 1 once a tail connection succeeds, 0 after 3 consecutive connection failures of a task. Since 2.1.0, only sources targeted by an enabled rule have the gauge. The label key is vl_source (not rule_name), and the cardinality drops from |rules| to |sources|.
PromQL migration examples
# v1.x (per-rule): valerter_victorialogs_up{rule_name="nginx-5xx"} == 0
# v2.0.0 (per-source): valerter_vl_source_up{vl_source="prod"} == 0
# v1.x (any rule down): min(valerter_victorialogs_up) == 0
# v2.0.0 (any source): min(valerter_vl_source_up) == 0
vl_source label added to every per-rule metric
Affected counters: valerter_alerts_sent_total, valerter_alerts_throttled_total, valerter_alerts_passed_total, valerter_alerts_failed_total, valerter_email_recipient_errors_total, valerter_lines_discarded_total, valerter_logs_matched_total, valerter_notify_errors_total, valerter_parse_errors_total, valerter_reconnections_total, valerter_rule_panics_total, valerter_rule_errors_total.
Affected gauge / histogram: valerter_last_query_timestamp, valerter_query_duration_seconds.
Dashboards and alerts that grouped by rule_name alone keep working but now get an extra vl_source dimension. PromQL using sum by (rule_name) (...) still rolls up correctly across sources. valerter_queue_size stays unlabeled (since 2.1.0 it sums the queues of every destination; per-destination values are in valerter_destination_queue_size).
Per-rule alert example
# v1.x
- alert: ValerterVictoriaLogsDown
expr: valerter_victorialogs_up == 0
for: 5m
# v2.0.0
- alert: ValerterVictoriaLogsSourceDown
expr: valerter_vl_source_up == 0
for: 5m
annotations:
summary: "Source {{ $labels.vl_source }} unreachable"
4. Notifier Output Changes
Mattermost footer
The footer now carries 4 segments instead of 3:
v1.x: valerter | <rule> | <timestamp>
v2.0.0: valerter | <rule> | <source> | <timestamp>
If you parse the footer string downstream, update the split logic.
Default webhook payload
The default webhook payload (used when body_template is omitted) gained a top-level vl_source field:
{
"alert_name": "<notifier_name>",
"rule_name": "...",
"vl_source": "prod",
"title": "...",
"body": "...",
"timestamp": "<ISO8601>",
"log_timestamp": "<ISO8601>",
"log_timestamp_formatted": "DD/MM/YYYY HH:MM:SS TZ"
}
Templates
The {{ vl_source }} template variable is available everywhere {{ rule_name }} is: rule templates (title, body, email_body_html), throttle.key, and notifier templates (subject_template, body_template). Always non-empty, equal to the source name currently processing the event.
If an event field is literally named vl_source, the synthetic value wins (matches the rule_name collision policy).
5. Rollback
If something goes wrong after the upgrade, you can roll back to v1.2.1 with no state migration required. The Prometheus metric labels are additive at the storage layer, except for the removed valerter_victorialogs_up gauge (which simply stops being produced when v2.0.0 runs).
Debian / Ubuntu
# Pin v1.2.1
curl -LO https://github.com/fxthiry/valerter/releases/download/v1.2.1/valerter_1.2.1_amd64.deb
sudo dpkg -i valerter_1.2.1_amd64.deb
sudo systemctl restart valerter
Static binary
curl -LO https://github.com/fxthiry/valerter/releases/download/v1.2.1/valerter-linux-x86_64.tar.gz
tar -xzf valerter-linux-x86_64.tar.gz
./valerter --validate -c /etc/valerter/config.yaml
You will need to revert the v2.0.0 config rewrite (the v1.x binary will reject the map shape). Keep a config.yaml.v1 backup before you upgrade.
Notes
- No on-disk state to migrate: throttle buckets are in-memory only.
- Prometheus historical data with the new
vl_sourcelabel remains valid (the label simply becomes empty for older samples in TSDB). - The v1.x
valerter_victorialogs_uptime series stops growing under v2.0.0 and resumes under v1.2.1.
See Also
CHANGELOG.md: full release notesexamples/multi-source/: complete working multi-source referencedocs/configuration.md: full configuration referencedocs/operations.md: service, upgrades, shutdowndocs/metrics.md: Prometheus metric catalog