Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.1.1] - 2026-10-23
Security release: dependency upgrade only, no functional change.
Security
rustlsupgraded to 0.23.46 (GHSA-2mjx-qc3c-rqvc, medium): TLS 1.3 handshake messages were incorrectly accepted across encryption level boundaries. rustls is the TLS stack of the VictoriaLogs, webhook, Mattermost, Telegram and SMTP connections.
[2.1.0] - 2026-10-06
Fixed
- Text logs no longer carry ANSI color codes in journald. The text log format always wrote color escape codes, so
journalctl -u valerterand log files showed them raw (^[[32m INFO). Colors are now used only when stderr is a terminal, and never whenNO_COLORis set; the JSON format is unchanged. - The daemon now exits with code 1 when it no longer watches anything. When every
(rule, source)task stopped without a shutdown request, or when the engine started without any task, valerter logged a warning and exited with code 0: the shipped systemd unit (Restart=on-failure) did not restart it andsystemctl statusshowed a cleaninactive (dead)while no alert was produced anymore. The engine now returns an error in both cases (No enabled rules found, engine will exit/All rule tasks completed unexpectedly, same text, now logged at ERROR), the process exits with code 1, the unit showsfailedand systemd restarts it. Exit code 0 now only follows a requested shutdown (SIGINT/SIGTERM). - No more ~7 s wait before exiting after the engine stops. The shutdown token is now cancelled as soon as the engine returns, whatever the cause: the metrics server stops at once, and the notification worker only performs the bounded drain of its queues (no wait at all when they are empty), instead of running until their 5 s and 2 s timeouts.
- Fatal errors are logged in the configured format, and the process no longer prints an unstructured
Error: ...line. A fatal error at startup, at runtime or in--validate(e.g.Failed to create notifiers: 1 errors) was logged and then printed again as a rawError: ...line on stderr, which broke--log-format jsonparsing; it is now logged once, at ERROR, through the logging system, and the exit code stays 1. The teardown of the async runtime is also bounded to 2 s, so a task stuck in the blocking pool (e.g. a DNS resolution) no longer delays the exit. - Upgrading the
.debno longer leaves the service stopped.prermno longer stops the service onupgrade, andpostinstrestarts it when it is active or enabled, including upgrades from 2.0.3 or earlier whoseprermalready stopped it. Two seconds after the restart,postinstchecks that the service is active and otherwise prints a visible warning pointing tojournalctl -u valerter, without failingdpkg. An enabled service that was stopped on purpose is started again by the upgrade; a service that is stopped and disabled stays stopped. - Invalid UTF-8 now drops only the faulty line. Each line of the tail stream is decoded on its own: a line that is not valid UTF-8 is dropped alone, while the other lines of the same network chunk and the pending partial line are kept (2.0.3 dropped the whole batch).
valerter_lines_discarded_total{reason="invalid_utf8"}now counts one unit per dropped line, and theDiscarding log data with invalid UTF-8warning is logged at most once per chunk with adiscarded_linesfield. - Lines over 1 MiB are dropped whole, without losing their neighbours or emitting a truncated fragment. The limit now applies to the length of a line, not to the buffer plus the incoming chunk: valid lines received in the same chunk are kept, and the rest of the oversized line is skipped up to its
\ninstead of being forwarded to the parser as a truncated line (which could raise an alert on partial JSON or a misleadinginvalid_json).valerter_lines_discarded_total{reason="oversized"}is incremented once per line. - A VictoriaLogs
urlending with/no longer produces//select/logsql/tail. Trailing slashes are dropped before the tail path is appended. - Custom
headersreplace the default and Basic Auth headers instead of duplicating them.accept: "application/json"now yields a singleAcceptheader, and a customAuthorizationreplaces the one built frombasic_auth(see Changed). - An invalid header name or value now fails the (rule, source) task at startup with an error naming the header (never its value), instead of an endless reconnection loop that never sent a request. Such headers are now refused when the configuration is loaded (see Changed); this is a defensive guard for the runtime path.
- The cause of a connection failure is logged before the backoff wait.
Connection failed/Stream read errorused to be logged after the sleep (up to 60 s late); they now precedeConnection failed, retrying. - The first empty stream end is followed by a ~1 s delay instead of 2 s. Consecutive empty EOFs now back off 1 s, 2 s, 4 s… up to 60 s.
throttle.key: "{{ rule_name }}"now really dedups across sources. Each(rule, source)task had its own throttle cache, so sources never shared a counter and the same outage seen byvlprodandvldevsent two alerts, contrary to MIGRATION.md and docs/configuration.md. A rule now has one throttle cache shared by all its sources. Behavior change for multi-source rules with a custom key that does not contain{{ vl_source }}(e.g.{{ host }}): their counter is now shared by the rule’s sources; add{{ vl_source }}to the key to keep one counter per source. The default key<rule_name>-<vl_source>:globalstill isolates sources. The reset on reconnection after an error now only drops the counters fed exclusively by the reconnecting source, and the throttle cache survives a task restart after a panic. See MIGRATION.md.- docs/architecture.md described the throttle as a sliding window. It is a fixed window, anchored on the first event of a key.
- A slow or unavailable notifier no longer blocks the other destinations. A single worker delivered the alerts one at a time, each alert waiting for every destination of the previous one (retries included): one endpoint that never answered (≈ 31.5 s per alert for an HTTP notifier, up to 60 s per
retry_afterfor Telegram) delayed every notifier, then made the queue overwrite alerts meant for healthy channels. Each notifier now has its own queue and its own delivery task: a slow, unavailable or saturated destination only delays or loses its own alerts. - A panicking notifier no longer stops all deliveries. A panic during a send killed the single worker, and every following alert failed with
notification queue closed. The panic is now caught, logged at ERROR (Notifier panicked while sending alert, fieldsnotifierandrule_name), counted invalerter_notify_errors_totalandvalerter_alerts_failed_total, and the destination moves on to its next alert. - Stopping or restarting the service no longer loses the queued alerts. On SIGTERM/SIGINT the notification worker stopped with the rule tasks and dropped every pending alert without a log, and
maingave the in-flight send only 5 s before cutting it off, so a restart during an incident (package upgrade, configuration change) discarded the most useful alerts. The rule tasks are now stopped first, then every destination finishes its in-flight send (retries included) and delivers the alerts left in its queue, in order, until it is empty. The drain is bounded to 20 s (fixed, not configurable) counted from the end of the rule tasks, and an empty queue exits at once. Logs:Waiting for notification worker to drain queue...(fieldqueued), thenNotification queue drained, or, when the deadline expires, the WARNShutdown drain timeout reached, alerts not deliveredwithundelivered(alerts left in the queues); the exit code stays 0. - A panicking rule task no longer freezes supervision and shutdown. After a panic, the engine slept 5 s inside its supervision loop: meanwhile it handled no other task (fatal errors and panics of other
(rule, source)tasks waited) and ignored a shutdown request, and simultaneous panics added up their delays. The restart delay now runs inside the restarted task: the other tasks stay supervised, a shutdown during the delay cancels the restart at once, and simultaneous panics each wait their own delay. A task waiting for its restart still counts as active, so it never triggersAll rule tasks completed unexpectedly. - A Telegram message rejected in HTML mode is now resent as plain text instead of being lost. With
parse_mode: HTML, a 400can't parse entitiesfrom the Bot API (an unescaped<or&from a custombody_template) abandoned the chat. The text is now resent once withoutparse_mode, so the alert is delivered: for an alert whose template hasbody_format: markdown, the title and theplainrendering of the body (no tags nor entities, truncated to 4096 codepoints); for any other alert, the same text, its tags shown literally. The WARNTelegram rejected HTML message, resending as plain textis logged (notifier, rule, chat and status; never the token, the API URL or the text). Only a 400 whosedescriptioncontainscan't parse entities(case-insensitive, read from a size-limited body) triggers the resend: any other 400 (chat not found,message text is empty, no JSON body), the other 4xx statuses andparse_mode: MarkdownV2keep failing immediately. The resend follows the usual retry policy; a 4xx on the resend fails the chat for good. - SMTP errors are classified by reply code instead of by message text. Any
5xxreply is now permanent and not retried (503,530,541… used to be retried three times), while4xxreplies and network, TLS or timeout errors are retried, even when their text mentionsauthenticationor contains a number like550(a454 4.7.0 temporary authentication failureused to be given up at once). - The webhook notifier now sends
Content-Type: application/jsonby default. Requests had noContent-Typeat all, which JSON endpoints (Slack, Discord, PagerDuty, REST APIs) reject or misread. The header is added whenheadersdefines noContent-Type(whatever its case); a configuredContent-Typestill wins and is not duplicated. Set one explicitly if your endpoint expects another content type. - The webhook examples of docs/notifiers.md and config/config.example.yaml now produce valid JSON. Slack, Discord, PagerDuty and Custom API examples wrote
"{{ body }}"between quotes, which breaks as soon as a title or body contains a quote, a backslash or a line break; they now use{{ body | tojson }}without quotes, the documented way to insert a value in a JSONbody_template. The PagerDuty example no longer sends a uselessAuthorizationheader, and its routing key now comes from${PAGERDUTY_ROUTING_KEY}, resolved in thebody_templatesource (see Changed). Rewrite your own JSON templates the same way. - The test render of templates no longer refuses valid templates. Conversions and arithmetic on fields (
{{ status | int }},| float,| round,| abs,| split,{{ count + 1 }}, a division between two plain identifiers like{{ total/count }}) failed validation because the placeholder values used for the test render are not numbers. The test render now only refuses errors that do not depend on the event’s values: syntax errors, unknown filters, tests, functions and methods, and the/operator on a field path. Global functions (range,namespace…) are also accepted again. This applies to templates,throttle.key,subject_templateandbody_template. - The hint for a top-level field containing
/no longer suggests a syntax that does not exist.{{ io/user-name }}was answered with{{ fields["io/user-name"] }}, but nofieldsvariable exists; the hint now explains that such a field cannot be referenced and suggests renaming it in the rule query (| rename "io/user-name" as io_user_name, then{{ io_user_name }}). The hint is only given for a field path (dotted left side, or a right side containing.,-or/):{{ total/count }}is a division and is accepted. - Rules loaded from
rules.d/now have a stable order. Their order changed from one run to the next (logs,--validatesummary, startup order); rules now follow the main file’s rules in declared order, then therules.d/rules sorted by file path, then by rule name within a file. Validation errors on templates and notifiers are also listed in alphabetical order of their name, including theNotifier configuration errorlines logged when notifiers are built (startup and--validate); when a webhook declares several invalid header names, the one reported is the first in alphabetical order. - Collisions between two files of the same
.d/directory use the singular, like the other collisions:duplicate rule name '...'(resp.template,notifier) instead ofduplicate rules name '...'. - docs/notifiers.md described a Mattermost footer that does not exist. The footer is
valerter | <rule_name> | <vl_source> | <log_timestamp_formatted>, notLog time: <timestamp>. The “Retry Behavior” section also claimed a 500 ms / 5 s backoff for every notifier: email uses 1 s / 30 s. - The metric series created at startup are now the ones that get incremented. valerter seeded
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}at startup, while the notifiers and the parser emit other label sets: those series stayed at 0 forever and the real ones only appeared at the first event, which brokerate()andincrease()and doubled unfiltered sums. Every series is now seeded at 0 with exactly its emission labels: notification counters per(enabled rule, resolved source, rule destination)withnotifier_nameandnotifier_type,valerter_parse_errors_totalper possibleerror_type,valerter_lines_discarded_totalfor bothoversizedandinvalid_utf8, the per-recipient and per-chat error counters for email and Telegram destinations, andvalerter_alerts_truncated_totalfor every Telegram notifier. A notifier used by no enabled rule gets no notification series. See MIGRATION.md. - Template render errors at send time are now counted. A webhook
body_template, an email subject or body, or a Telegram message text that failed to render was only logged; it now incrementsvalerter_notify_errors_totalandvalerter_alerts_failed_totalonce, like any permanent failure. Nothing is sent and the error message is unchanged. - docs/metrics.md is complete again. It now lists
valerter_alerts_truncated_total, the new counters and every metric with a HELP text, describesvalerter_notify_errors_totalas permanent failures (it said “transient”), and states the once-per-alert-and-notifier counting. docs/architecture.md referred to the removedvalerter_victorialogs_upgauge instead ofvalerter_vl_source_up, and theMetricsServerdoc comment promised a/healthendpoint as part of valerter’s interface: only/metricsis documented. ValerterVlSourceDownno longer fires for sources no enabled rule targets.valerter_vl_source_upwas initialized to 0 for every declared source, so a source that no enabled rule tails (all its rules disabled, or excluded by every rule’svl_sources) stayed at 0 forever and looked down. Only the sources targeted by at least one enabled rule now get the series (and thesource_countfield ofMetrics initialized to zerocounts only them).- A slow or endless error body from VictoriaLogs no longer blocks the tail task. The body of a non-2xx response was read until its end: a proxy answering
502with a chunked body that never ends froze the(rule, source)task. At most 4 KiB are now read, for at most 5 s, before loggingHTTP error from VictoriaLogsand backing off. ${VAR}substitution is done in a single pass. A variable whose value contains${...}had that text substituted in turn (A='${B}'made${A}resolve to the value ofB); the inserted value is now kept as is.- The Debian package stops and disables the service before removing it, and leaves systemd alone when it is not running.
prerm removeonly stopped anactiveservice: a unit inactivating (auto-restart)between two restarts kept running onto a deleted binary, and the unit was disabled only after the files were gone.prermnow always stops the service onremove/deconfigureand disables it onremove;postrm removeonly reloads systemd. The maintainer scripts now act on the service only when systemd is the running init (/run/systemd/system), so an install in a container or a chroot no longer printsvalerter failed to start after upgrade. - The plain-text
bodyinserted in an email body is now HTML-escaped. When a template had noemail_body_html, the email body template received the plainbodymarked as already escaped, so its<,>and&were inserted as HTML. Only the message sent when a rule template fails to render (Template render failed: ...) could reach this path,email_body_htmlbeing required otherwise; it is now escaped (<x>). - Telegram truncation no longer cuts HTML tags or entities. With
parse_mode: HTML, a message longer than 4096 codepoints was cut at the 4095th codepoint of the raw text, tags and entities included: a<pre>left open or a cut&made Telegram reject the message, and a longhrefcounted against the limit. Only the visible text is now counted, as Telegram does (a tag counts zero, an entity one); the cut never falls inside a tag or an entity, and the tags still open after the…are closed, so the truncated message stays valid HTML. Truncation withparse_mode: MarkdownV2orMarkdownis unchanged. - docs: the documentation is reorganized and checked against the code. Templates have their own page (docs/templates.md: rule and notifier templates, a single variables table, filters and functions, Markdown bodies, template validation), and so does running the daemon (docs/operations.md: files and permissions, systemd, logs, exit codes, shutdown drain, delivery queues,
.debupgrades including the conffile prompt, containers,--validatewithsudoand in CI); docs/configuration.md keeps the configuration file and gains ametricssection, and docs/notifiers.md a common Delivery section (queues, order, retries, timeouts, drain). Fixed along the way: rule template examples reading{{ title }}/{{ body }}(empty there), the LogsQL example (| unpack_json | filter cpu:>90), the parser description (regex first, a non-matching line dropped), the configuration permissions (640 root:valerter), the webhook methods (POST and PUT only), the email body template examples (accent_coloris none,bodyis already HTML), the minimum Rust version (1.88) and the SMTP test command. MIGRATION.md gains a checklist, the new features, the other behavior changes and a rollback section for 2.1.0. - docs: Telegram no longer advises HTML in
body.docs/notifiers.mdandconfig/config.example.yamltold to put<b>/<code>markup in the rule template’sbody, but the default Telegrambody_templateescapesbody({{ body|e }}), so the tags were shown literally. The documentation now writes the markup in the Telegrambody_templateand escapes each inserted value (<code>{{ log.host|e }}</code>). See MIGRATION.md.
Changed
- A configuration whose rules are all disabled is now refused.
Config::validate()reportsall rules are disabled: enable at least one rule in config.yaml or rules.d/, sovalerter --validateflags it and the daemon refuses to start (exit code 1) instead of starting and exiting immediately with code 0. See MIGRATION.md. Connection failed, retryingnow logs the delay in milliseconds (delay_ms) instead of whole seconds (delay_secs), which logged0for sub-second delays. Log filters ondelay_secsmust be updated. See MIGRATION.md.- A custom
Authorizationheader now masksbasic_auth. Both used to be sent and the server picked one; the custom header now wins and a warning (without any value) is logged at the start of each affected (rule, source) task. --validatenow runs every startup check. Besides loading and validating the configuration, it compiles it, builds every notifier (resolving${VAR}placeholders in notifier secrets, readingbody_template_file), checks that every rule destination exists and that templates sent to email destinations defineemail_body_html, and emits themattermost_channel ignoredwarning, with the same messages and exit code 1 as the daemon. It still starts nothing and makes no network call. Breaking for CI pipelines that validated without secrets: environment variables referenced by notifiers must now be defined when running--validate. See MIGRATION.md.- The
--validatesummary gains aNotifiers: <n> [<name>=<type>, ...]line afterTemplates. - Stricter configuration validation. Breaking: configurations accepted by 2.0.3 can be refused at startup; run
valerter --validate -c /etc/valerter/config.yamlwith the new binary before upgrading. Refused at load time (Config::validate()): a notifier listed twice in a rule’snotify.destinations, which delivered every alert twice to it (rule '<rule>': notify.destinations contains duplicate entry '<name>' (each notifier may appear at most once));defaults.throttlewithcount: 0orwindow: 0s(previously a runtime warning;defaults.throttle.count must be >= 1 ...,defaults.throttle.window must be > 0 ...) and itskeychecked like a rule key (defaults.throttle.key: ...,defaults.throttle.key render: ...); an unknown filter, test or function inthrottle.key, for enabled and disabled rules (invalid template in rule '<rule>': throttle.key render: ..., previously every event fell back to the key<rule>:error); an invalid header name or value in a VictoriaLogs source (victorialogs.<source>.headers: invalid header name '<name>'/invalid value for header '<name>', the value is never printed); a source URL that still contains${after substitution. Refused when notifiers are built, at startup and by--validate: an unknown filter, test or function in a webhook, Telegram or emailbody_template(inline,body_template_fileor default;invalid notifier '<name>': body_template render: ..., previously every send failed); a Telegramparse_modeother thanHTML,MarkdownV2orMarkdown(parse_mode '<value>' is not supported (expected HTML, MarkdownV2 or Markdown), previously an HTTP 400 for every alert); a webhookurlor Mattermostwebhook_urlwhose${VAR}resolves to a non-URL or a scheme other thanhttp/https(url: invalid URL: .../webhook_url: invalid URL: ..., the URL is never printed). The test render of templates,throttle.key,subject_templateandbody_templatenow also goes on after a built-in filter applied to a field (| int,| float,| round,| split,| upper,| items…) and is run 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: an unknown filter after such a filter ({{ status | int }}-{{ host | truncat(10) }}, which made every event fall back to the throttle key<rule>:error), in anelsebranch, in{% if not x %}, underis not definedor in a loop body is now refused (... render: ...). See MIGRATION.md. - Telegram
parse_modeis case-insensitive and sent in canonical form:htmlis sent asHTML,markdownv2asMarkdownV2. - Startup now reports every notifier, destination and email template error in one pass instead of stopping at the first failed stage (same messages and exit code). A notifier that fails to build is no longer also reported as an unknown destination.
- The throttle cache bound is now 10,000 keys per source of a rule, for the rule’s shared cache (previously 10,000 per
(rule, source)task): same total memory budget, still not configurable. - The notification queue capacity is exactly 100 alerts per destination. The single queue announced 100 but held 128 (the channel rounded up to a power of two); there is now one queue per notifier holding exactly 100 alerts, with drop-oldest applied per destination. Up to 100 × number of notifiers alerts can be pending. Delivery order is guaranteed per destination, no longer across destinations.
valerter_queue_sizeandvalerter_alerts_dropped_totalnow count deliveries summed over every destination queue (an alert routed to two destinations counts twice). Their names and selectors are unchanged, but absolute thresholds onvalerter_queue_sizechange meaning: the sum can reach 100 × number of notifiers (128 at most before), so a rule likevalerter_queue_size > 50no longer means “queue half full”. Rewrite it on the new per-destination gauge, e.g.max(valerter_destination_queue_size) > 50;rate(valerter_alerts_dropped_total[...])alerts keep their meaning. TheQueue full, dropping N oldest alertswarning gains anotifierfield. See MIGRATION.md.- A shutdown can now take up to ~20 s when alerts are queued or an endpoint is slow (rule tasks + at most 20 s of drain; the metrics server stops at once and the runtime teardown is bounded to 2 s), still within the
TimeoutStopSec=30of the shipped systemd unit. Thesystemctl restartrun by the Debianpostinston upgrade can therefore blockdpkg/aptfor up to ~20 s. Container runtimes need a 30 s stop timeout to benefit from the drain (docker stop --stop-timeout 30,stop_grace_period: 30s; Docker’s default is 10 s). See MIGRATION.md. - A second SIGTERM/SIGINT now forces an immediate exit. It used to be ignored; whatever the shutdown phase, it now logs the WARN
Second shutdown signal received, forcing immediate exitand exits at once with code 1, without waiting for the in-flight sends or the drain. - Restarts after a panic now back off exponentially. A
(rule, source)task that panics is restarted after 5 s, doubled at each consecutive panic up to 5 min, instead of every 5 s forever; a task that ran 10 min without panicking starts over at 5 s. Restarts stay unlimited: a task is never abandoned, andvalerter_rule_panics_total{rule_name, vl_source}keeps increasing, which is the signal to alert on (see docs/metrics.md).Respawning rule-source task after panic delaygains aconsecutive_panicsfield andRule-source task respawned after panicis now logged when the delay is over. - Breaking:
valerter_reconnections_totalno longer counts clean stream ends. It now only counts reconnections after a failure (connection error, HTTP error response, error while reading the stream); a clean end of the stream (EOF without error) is counted in the newvalerter_stream_ends_total{rule_name, vl_source}. This reverts the 2.0.3 choice of counting clean EOFs invalerter_reconnections_total, made in a release announced without breaking changes: dashboards and alerts built since 2.0.3 see lower values, especially behind a proxy that closes idle streams.valerter_reconnections_total + valerter_stream_ends_totalgives the 2.0.3 total. See MIGRATION.md. - Breaking: Telegram alerts are counted once per alert, like email.
valerter_alerts_sent_totalincreases by 1 when at least one chat received the alert (it used to increase once per successful chat), andvalerter_notify_errors_total/valerter_alerts_failed_totalincrease by 1 only when every chat failed (they used to increase once per failed chat, even when the alert reached other chats). Failed chats are counted in the newvalerter_telegram_chat_errors_total. A chat delivered through the plain-text fallback counts as a success. See MIGRATION.md. - Breaking: the reduced-label series seeded at startup are removed (
valerter_alerts_sent_total{rule_name, vl_source},valerter_parse_errors_totalwithouterror_type,valerter_alerts_failed_total{notifier},valerter_notify_errors_total{notifier}, see Fixed). Queries on thenotifierlabel must usenotifier_name. See MIGRATION.md. - Breaking:
${VAR}placeholders in a webhookbody_templateare now resolved from the environment. Likeurlandheaders, the template source is resolved once when the notifier is built, at startup and by--validate; an undefined variable refuses the configuration withinvalid notifier '<name>': body_template: invalid configuration: undefined environment variable: <VAR>(exit code 1). A literal${...}used to be sent as is; write{{ '$' }}{VAR}to send a literal${VAR}(the substitution does not recognize it and the render produces${VAR}). Values rendered from logs are never resolved, and the resolved template is never logged, except that the message of a syntax error may quote a fragment of it (a value containing{{,{%or breaking the syntax). A value containing{{,{%or"is inserted before Jinja parses the template: keep such values out ofbody_template. See MIGRATION.md. - Breaking: a rule’s
notify.mattermost_channelis now applied. It was documented as a channel override but silently ignored: alerts of a rule that sets it now go to that channel. Priority: the rule’smattermost_channel, then the notifier’schannel, then the webhook’s default channel. Other notifier types ignore it. See MIGRATION.md. valerter::initialize_metricsnow takes a&MetricsInventory(sources,(rule, source)pairs with their parser kind,(rule, source, destination)triplets and registered notifiers) instead of three slices of names. The newMetricsInventory,RuleSourceSeries,DeliverySeriesandNotifierSeriestypes are exported by the crate.- Notifier templates are compiled once, when the notifier is built, instead of at every send: the webhook, Telegram and email
body_templateand the emailsubject_template. Each alert also unflattens its dotted keys once instead of once per rendered field. Rendering and error messages are unchanged.
Added
- Per-destination queue metrics.
valerter_destination_queue_size{notifier_name, notifier_type}(alerts pending for a destination, 0 to 100) andvalerter_destination_alerts_dropped_total{notifier_name, notifier_type}(alerts dropped because its queue was full), both initialized to 0 for every notifier. - Startup INFO log for throttle keys shared across sources. Once per rule targeting at least two sources whose custom throttle key does not reference
vl_source: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, withrule_name,source_countandthrottle_keyfields. The key is analysed statically (no test render). Webhook body is not valid JSONwarning. When the effectiveContent-Typeof a webhook is JSON (application/jsonor any+jsontype) and the renderedbody_templateis not valid JSON, a WARN is logged with the notifier and rule names (never the body) and the request is still sent.valerter_stream_ends_total{rule_name, vl_source}counts clean ends of the VictoriaLogs stream (EOF without error), each followed by a reconnection. Initialized to 0 for every(rule, source)pair.valerter_telegram_chat_errors_total{rule_name, vl_source, notifier_name}counts the Telegram chats an alert could not be delivered to, the counterpart ofvalerter_email_recipient_errors_total.- HELP text for
valerter_email_recipient_errors_totalandvalerter_lines_discarded_total, which had none. - Mattermost resends an alert to the notifier’s default when the rule’s channel is rejected. When an alert carrying a rule’s
mattermost_channelgets a 4xx response other than 429 (webhook locked to its channel, channel that does not exist), it is resent once to the notifier’schannelif it has one (and it differs from the rule’s), otherwise withoutchannel(the webhook’s default channel), instead of being lost, and the WARNMattermost rejected channel override, resending to notifier defaultis logged (notifier, rule, requested channel, fallback channel when there is one, and status; never the webhook URL). The resend follows the usual retry policy; a 4xx on the resend fails the alert (client error: <status>). The alert is counted once, as sent or failed. A notifier’s ownchannelkeeps failing immediately on a 4xx. logvariable in notifier templates. The webhook, Telegram and emailbody_templateand the emailsubject_templatecan read every field of the event underlog:{{ log.host }},{{ log["k8s.pod"] }}or{{ log.k8s.pod }},{{ log | tojson }}for the whole event. Markup and structured payloads (PagerDutycustom_details, HTML tables, Telegram tags) can now be built in the template of the channel they target, each value escaped for it, instead of being baked into the rule’sbody(#24). The rule template is unchanged:logis not injected there. Event fields are never logged: the debug output of an alert shows only their count.md_escapeandmdv2_escapefilters, available in every template:md_escapeescapes the Markdown characters Mattermost recognises (\ ` * _ { } [ ] ( ) # + - . ! > | ~),mdv2_escapethe 18 reserved characters of Telegram MarkdownV2 and\. HTML escaping stays| e. They are known to the test render of--validate.- Warning for unknown variables in notifier templates. At startup and in
--validate, abody_templateorsubject_templatereading a variable that does not exist at its level (typically{{ host }}instead of{{ log.host }}) logsNotifier template references unknown variablewith the notifier, the field and the variable. The configuration is still accepted. body_format: markdown: a rule body written once in Markdown, rendered for each notifier (#24). The new optionalbody_formatkey of a template (text, the default, ormarkdown) makesbodya Markdown source: every value inserted with{{ ... }}is escaped (each ASCII punctuation character gets a backslash,| tojsonincluded;| eescapes for Markdown; only| safeinserts a value as is), so a value of the log is shown as text on every channel unless inserted with| safe(as in a text body, the client still detects bare URLs,@channeland mentions). The line breaks of a value stay line breaks, but its leading blanks and empty lines are written as no-break spaces (U+00A0), so a multi-line value cannot end an emphasis, a list item or a quote, nor open a code block. valerter parses the body (CommonMark plus~~strikethrough~~: paragraphs, line breaks, bold, italics, code, code blocks, links, quotes, lists, headings, rules; raw HTML and tables shown as text, a single line break kept) and renders it for each notifier: Markdown escaped for Mattermost, the HTML subset of the Bot API for Telegram (always well formed and nested as the Bot API allows, so the default{{ body|e }}template works unchanged), HTML for email, plain text for a webhook by default. Links are only emitted forhttp,httpsandmailto. Nesting is limited to 32 levels (a deeper element keeps its text), so no body can exhaust the stack. A Markdown template needs noemail_body_htmlfor an email destination.titlestays plain text. Each rendering is computed once per alert and shared by its destinations. Templates withoutbody_formatrender exactly as before. See docs/configuration.md.formatper notifier. The optionalformatkey chooses the rendering of a Markdown alert’s body:markdownfor Mattermost (only value);telegram_html(default withparse_mode: HTML) orplain(default with anotherparse_mode) for Telegram;htmlfor email (only value);plain(default),markdownorhtmlfor a webhook. A value the type does not accept is refused at startup and by--validate(invalid notifier '<name>': format '<value>' is not supported for <type> notifiers (expected <list>)), as istelegram_htmlwith aparse_modeother thanHTML(format 'telegram_html' requires parse_mode HTML). See docs/notifiers.md.code,codeblockfilters andmd_linkfunction to build Markdown from values:{{ host | code }}(a code span showing the value literally, whatever backticks it contains),{{ _msg | codeblock('json') }}(a fenced code block, language optional),{{ md_link(text, url) }}(a link with a literal text and a URL whose spaces, control characters,<,>,(,)and\are percent-encoded; a scheme other thanhttp,httpsormailtogivestext (url)without a link; encode the values put in the URL with minijinja’surlencodefilter, now enabled). In a Markdown body, their content stays literal wherever they are written (after a list marker, in a quote, on an indented line, in the middle of a line):codeblockcuts the paragraph around its block, or becomes a code span where no block can stand (emphasis, heading, link text). Elsewhere their result is an ordinary string, escaped like any value (HTML inemail_body_html). They are known to the test render of--validate, which also renders Markdown bodies with their escaping active.
Removed
- Breaking:
valerter_notifier_config_errors_totalis removed. It was incremented while notifiers were built, before the metrics exporter was installed, and the error stops valerter with exit code 1: no scrape could ever see it. Notifier configuration errors remain reported by the ERROR logNotifier configuration errorand the exit code (the unit showsfailed). See MIGRATION.md.
Security
- A log line with a dotted key of thousands of segments no longer aborts the daemon. Dotted keys (
a.b.c) are expanded into nested objects for templates, recursively: a key of 20,000 segments (40 KB of a log line) overflowed the stack of the worker and aborted the whole process, which no task supervision can catch. Sincelogis built for every alert, any matched line could trigger it. A key of more than 32 segments is no longer expanded: only its flat key exists ({{ log["a.b.c…"] }}) and the WARNskipping dotted-key expansion: too many segmentsis logged, the key truncated to 128 bytes. - VictoriaLogs source URLs are masked in the streaming logs. With
RUST_LOG=debug,Connecting to VictoriaLogs tail endpointlogged the full source URL, credentials and query string included (often resolved from${VAR}), and the transport errors ofConnection failed/Stream read errorembedded it too. The URL is now logged masked (http://***@vl:9428/select/logsql/tail?***, the LogsQL query in a separatequeryfield) and transport errors are logged without it. --validateno longer prints VictoriaLogs source credentials. The summary showed each source URL after${VAR}resolution, sohttp://user:secret@vl:9428?token=abcreached stdout (often captured by CI). Credentials and query strings are now replaced by***and fragments dropped (http://***@vl:9428/?***); URLs without such parts are printed unchanged.
[2.0.3] - 2026-08-26
Hardening release driven by a full code review. No new features, no breaking changes.
Security
- Webhook and Mattermost URLs no longer leak into logs on network errors. Both notifiers logged the raw
reqwest::Error, whoseDisplayincludes the request URL (i.e. the webhook secret). They now logerror.without_url()like the Telegram notifier already did. validate_urlno longer echoes the URL in its error message. A malformedwebhook_urlused to print the full value (token included) at startup. It also now restricts schemes tohttp/https; other schemes were accepted at validation and failed on the first alert.
Fixed
${ENV_VAR}placeholders are now resolved in VictoriaLogs sources (url,basic_auth.username/password,headers). This was documented and shown inexamples/multi-sourcebut never implemented: the literal${VL_PASS}was sent to VictoriaLogs, producing a 401 retry loop. An undefined variable is now a load error naming the source.webhook_url: "${MATTERMOST_WEBHOOK}"(the documented form) no longer fails validation.validate_urlran before env resolution and rejected the placeholder as an invalid URL. Values containing${are skipped at validation and checked after resolution.- Invalid UTF-8 in the tail stream no longer kills the rule permanently. A single bad byte sequence raised
StreamError::Utf8Error, which the supervisor treated as fatal without respawn: the (rule, source) pair silently stopped alerting until restart. The offending batch is now dropped, logged, and counted invalerter_lines_discarded_total{reason="invalid_utf8"}; streaming continues. - Clean stream end (HTTP 200 then EOF) no longer resets the throttle cache nor spins. It was flagged as a failure, so the next connect fired
on_reconnect→Throttler::reset()and let duplicate bursts through; it also reconnected with no delay. A benign EOF now reconnects after the base backoff (growing only while the server keeps closing immediately with no data), keeps throttle state, and incrementsvalerter_reconnections_total. - Stream buffer is cleared on every (re)connection, so a partial line from a dead connection is never glued to the first bytes of the new one.
- HTTP error response body is now read and logged before the backoff sleep, not after (the v2.0.2 change could log
<empty body>once the server had closed the connection). - HTTP 429 from Mattermost/webhook endpoints is now retried with backoff instead of being treated as a permanent client error and dropping the alert.
connect_timeout(10s) added to the tail HTTP client. A blackholed host previously blocked each attempt for the OS SYN timeout (~2 min).- Validation now rejects duplicate rule names within
config.yaml(they silently shared throttle keys and metrics),throttle.count: 0andthrottle.window: 0s.
[2.0.2] - 2026-08-26
Patch release: actionable validation errors for two user-reported configuration pitfalls, plus dependency bumps clearing all open Dependabot alerts. No new features, no breaking changes.
Fixed
-
Templates referencing fields with
/in their name now get an actionable validation error (#41).{{ ocp.annotations.authentication.openshift.io/username }}fails in Jinja because/is the division operator.valerter --validatenow appends a hint with the working bracket-notation rewrite ({{ ocp.annotations.authentication.openshift["io/username"] }}). Documented indocs/configuration.md#fields-with-special-characters. -
Queries using pipes unsupported by
/tailare rejected at validation time (#42).stats,sort,top,uniq,limit,offset,first,last,facets,join,field_names,field_values,block_stats,blocks_countandunionneed the full result set and are refused by the VictoriaLogs/tailendpoint with HTTP 400, which previously showed up as an endless opaque reconnect loop. The rule now fails--validatewith an explicit message. Documented indocs/configuration.md#logsql-query-restrictions. -
Non-2xx VictoriaLogs responses now log the response body (truncated to 512 chars) alongside the status code, so the actual server-side error message is visible in
journalctlinstead of a barestatus=400 Bad Request.
[2.0.1] - 2026-04-17
Hardening patch after the v2.0.0 release. No new features, no breaking changes. Three fixes bundled into one coherent “post-v2.0.0 durability” release.
Security
- Notifier config secrets wrapped in
SecretString.TelegramNotifierConfig.bot_token,MattermostNotifierConfig.webhook_url,WebhookNotifierConfig.url,WebhookNotifierConfig.headersvalues, andSmtpConfig.passwordare nowSecretStringrather than rawString. Aformat!("{:?}", config)or a tracing context that captures the parsed config renders[REDACTED]instead of the actual value. Prior to this, the token lived as a plainStringuntil notifier construction time, so any debug-log or error context that carried the config body would leak the secret. A regression-guard unit test (notifier_config_debug_never_leaks_secrets) runs canary values through each notifier config’sDebugoutput and asserts none appear.
Fixed
-
Migration error on v1.x configs now points to MIGRATION.md and covers rollback (D-doc-1). Loading a v1.x
victorialogs.url-shape config against v2.x used to emit a serde-flavoured error that referenced the CHANGELOG only. The new message leads with a human-readable “Configuration incompatible with valerter v2.0.0”, includes the before/after YAML diff, a direct link toMIGRATION.md, and a rollback hint pointing at the GitHub releases page. -
valerter_vl_source_upgauge debounced to 3 consecutive failures (D-vl-obs-1). The gauge used to flip to0on any single HTTP 5xx, connection error, or mid-stream EOF, which made sources behind a flaky load balancer flap their reachability state and page operators on transient events. The flip is now gated byVL_SOURCE_UP_FAILURE_THRESHOLD = 3(fixed, not configurable): three consecutive failures before0, any single success resets the counter back to1and re-arms the debounce. Contract unchanged for persistently-down sources.
[2.0.0] - 2026-04-16
Security advisory
Raw _msg piped into email_body_html renders unescaped in email clients.
The example config switched body: "{{ _msg }}" in v1.2.0 (#26 fix), and operators may reasonably mirror that in email_body_html. The email notifier marks body as safe (pre-escaped HTML) before injection into the email envelope, so a log line containing raw HTML or <script> tags would render unescaped in the recipient’s mail client.
This is pre-existing behaviour from v1.x, not a regression introduced in v2.0.0, but the surface is wider now that the example actively uses _msg.
Mitigation: if your VictoriaLogs ingests untrusted content (web request bodies, user-controlled fields), wrap the offending field with | escape, or render via plain body (not email_body_html) for email destinations until the email path is hardened in a follow-up.
Breaking changes
-
victorialogsis now a map of named sources. A single valerter instance can tail multiple VL backends and route alerts per source. The v1.x single-URL shape (victorialogs.url: ...at the top level) is rejected at load with an actionable migration error.Migrate from:
victorialogs: url: "http://victorialogs:9428" basic_auth: username: "u" password: "p"To:
victorialogs: default: url: "http://victorialogs:9428" basic_auth: username: "u" password: "p"Then optionally target sources per rule via
vl_sources: [name, ...], or omit the field to fan out across every configured source. Credentials, TLS, and headers are per-source, self-contained in eachVlSourceConfig. -
Default throttle key is now
{rule}-{source}:global(was{rule}:globalin v1.x). Multi-source deployments get isolated throttle buckets per source with no extra config. Users who want cross-source dedup must overridethrottle.keyexplicitly (e.g.key: "{{ rule_name }}"). -
Source names are restricted to
^[a-zA-Z0-9_]+$. No dashes, colons, dots, or spaces allowed. Validated at load. The constraint avoids ambiguity in the default throttle key format above. -
Notifier output formats extended with
vl_source. The Mattermost footer now readsvalerter | <rule> | <source> | <timestamp>instead ofvalerter | <rule> | <timestamp>. The default webhook payload exposesvl_sourceas a top-level JSON field. Downstream parsers / dashboards that match exact strings in either output need to update. -
All per-rule Prometheus metrics now also carry a
vl_sourcelabel. 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 byrule_namealone keep working but get an extravl_sourcedimension; PromQL usingsum by (rule_name) (...)still rolls up correctly.valerter_queue_sizestays unlabeled (the queue is shared, not per-source). -
valerter_victorialogs_up{rule_name}removed and replaced byvalerter_vl_source_up{vl_source}. The new gauge is per-source (one value per configured source, regardless of how many rules tail it) since reachability is a property of the source, not the rule. Alerts and panels need to migrate from per-rule to per-source semantics. 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) == 0The label key is now
vl_source(notrule_name), and the cardinality drops from|rules|to|sources|. -
defaults.max_streamscap introduced (default 50). Total VictoriaLogs streams = sum of(rule, source)pairs spawned for enabled rules. Breaching the cap fails the config at load with both the actual count and the cap value. Configurable viadefaults.max_streams: <usize>. Disabled rules do not contribute. Prevents accidental fan-out from DoSing a backend.
Added
- Multi-source VictoriaLogs support (issue #34). The engine spawns one task per
(rule, source)pair with per-source cancellation and reconnect isolation, so a single unhealthy source does not stop alerts on the others. {{ vl_source }}template variable available everywhere{{ rule_name }}is: layer 1 templates (title,body,email_body_html),throttle.key, and notifier-level layer 2 contexts (subject_template,body_template). Always non-empty, ownedString, equal to the source name currently processing the event. Synthetic value wins over any event field literally namedvl_source(matches therule_namecollision policy).AlertPayload.vl_sourcepropagated end-to-end so notifiers can render the source name. See Breaking changes above for the related output format updates on Mattermost and webhook destinations.valerter_vl_source_up{vl_source}per-source reachability gauge. Initialized to 0 for every configured source at startup; engine flips to 1 on tail connect success and back to 0 on permanent failure or stream error. Replaces the v1.x per-rulevalerter_victorialogs_up.±10%uniform jitter on reconnect backoff (per(rule, source)task). Sources behind a flapping load balancer no longer reconnect in lock-step, breaking the thundering-herd alignment over a few cycles. Hardcoded jitter range; not configurable in this release.tests/metrics_snapshot.rsintegration test. Spins up a 2-source 1-rule engine, scrapes/metrics, and asserts the set of metric names + label keys (not values) against an inline expected string. Catches accidental relabel/rename in future PRs.examples/multi-source/config.yamlreference and top-levelMIGRATION.mdfor v1.x upgraders.
[1.2.1] - 2026-04-16
Fixed
{{ rule_name }}available in top-level templates and throttle key (issue #31).rule_nameis now injected into the render context oftemplates.<name>.title,body, andemail_body_html, and also into thethrottle.keytemplate, not just the notifier-levelsubject_template/body_template. Configs that referenced{{ rule_name }}in a top-level template previously rendered an empty string; they now render the rule name. If an event field happens to be literally namedrule_name, the synthetic rule name wins, matching the collision policy of the existing notifier-level contexts.- Dotted event fields resolve in
throttle.key. The unflatten step introduced in v1.2.0 for template rendering (issue #25) now also applies to the throttle key template, sothrottle.key: "{{ nginx.http.status_code }}-{{ hostname }}"works consistently withtitle/body.
[1.2.0] - 2026-04-15
Breaking changes
- Template field
body_htmlrenamed toemail_body_htmlto reflect that only the email notifier consumes it (Telegram, Mattermost, and webhook always ignored it). Migration: in every template, replacebody_html:withemail_body_html:. This applies to templates defined inline inconfig.yamland to any split files undertemplates.d/. Configs using the old name are rejected at load time with a clear error that listsemail_body_htmlamong the expected fields, sovalerter --validatewill point out every template that needs updating on the first run.
Fixed
- Dotted field access in templates (issue #25) — fields like
server.hostnameorhttp.request.methodcan now be referenced directly in Jinja templates using dotted notation, matching the shape users see in log payloads. - Empty Telegram message guard (issue #26) — Telegram no longer 400s when a
rendered body is empty. The notifier now substitutes a fallback string and
records the drop reason, and the template documentation explicitly calls out
that
body_html(nowemail_body_html) is email-only so users do not accidentally leavebodyempty.
1.1.0 - 2026-04-15
Added
- Telegram notifier (issue #22) — native
type: telegramnotifier using the Bot APIsendMessageendpoint. Supports multi-chat delivery (one sequential HTTP call perchat_id), HTMLparse_modeby default, 429Retry-Afterhandling, automatic codepoint-safe truncation at Telegram’s 4096 character limit, and a newvalerter_alerts_truncated_totalPrometheus counter.
Known Limitations
- Templates define a single
bodyfield that is shared across all notifiers. If you write a Markdown-flavored body (e.g.**bold**, triple-backtick fences) for Mattermost, Telegram will render those markers literally because it is configured withparse_mode: HTML. Workaround: overridebody_templateon the Telegram notifier with HTML-friendly Jinja, for examplebody_template: "<b>{{ title|e }}</b>\n<pre>{{ body|e }}</pre>". A proper render-pipeline-per-notifier abstraction is planned for 1.2.
1.0.0 - 2026-04-14
Promote 1.0.0-rc.5 to stable. No functional changes.
Security
- Dependency updates via
cargo updateto pick up patched versions:aws-lc-sys0.35.0 → 0.39.1 (GHSA advisories on AWS-LC crypto/x509)quinn-proto0.11.13 → 0.11.14 (QUIC transport parameter DoS)rustls-webpki0.103.8 → 0.103.11 (CRL scope check)bytes1.11.0 → 1.11.1 (BytesMut::reserveinteger overflow)
1.0.0-rc.5 - 2026-01-20
Final RC - Hardening and observability improvements before 1.0.0 stable.
Added
- TCP keepalive - Prevents silent connection drops on long-lived VictoriaLogs streams (60s keepalive on reqwest client)
- StreamBuffer size limit - 1MB max line size with
valerter_lines_discarded_total{reason=oversized}metric to prevent OOM from malformed input - Strict config validation -
deny_unknown_fieldsrejects typos, URL format validation for VictoriaLogs and webhook endpoints - Rust load-generator - High-performance testing tool achieving 100k logs/sec for stress testing
- Comprehensive debug logging - Strategic debug/trace logs across all modules for troubleshooting
- Performance test suite - Load testing scripts and documented results (10k+ logs/sec sustained)
Fixed
- Tracing span propagation - Use
instrument()for async-safe span propagation in all notifier functions (engine, queue, mattermost, webhook, email) - Notification success logging - Promoted from debug to info level for production visibility
- Metrics label cleanup - Removed unused
rule_namelabel fromalerts_dropped_total(global metric, not per-rule) - RUST_LOG support - Now properly respects environment variable for log filtering
Changed
- Documentation overhaul - Rewrote README “Why Valerter” section, updated metrics docs with
valerter_email_recipient_errors_totalandvalerter_notifier_config_errors_total, added performance report
1.0.0-rc.4 - 2026-01-16
Feature freeze - From this release, only bug fixes until 1.0.0 stable. No new features or refactoring.
Added
- Multi-file configuration - Split rules, templates, and notifiers into separate files in
.d/directories (rules.d/,templates.d/,notifiers.d/) - Collision detection - Explicit errors when duplicate names are found across config files
- Warning for unused mattermost_channel - Warns when
mattermost_channelis set but no Mattermost notifier in destinations
Changed
- BREAKING:
notify.templaterequired - Each rule must now specify its template explicitly (no moredefaults.notify.templatefallback) - BREAKING:
notify.destinationsrequired - Each rule must specify at least one destination - BREAKING:
defaults.notifyremoved - The entiredefaults.notifysection has been removed - BREAKING:
notifierssection required - At least one notifier must be configured - BREAKING:
templatessection required - At least one template must be defined - BREAKING:
MATTERMOST_WEBHOOKenv var removed - Usenotifierssection instead - BREAKING:
notify.channelrenamed - Nownotify.mattermost_channelfor clarity - Strict field validation - Unknown fields in
notifysection now cause parsing errors
Fixed
- Debian package - Creates
.d/directories on install
1.0.0-rc.3 - 2026-01-15
Changed
- Simplified tarball - Contains only binary + config.example.yaml (removed install.sh, uninstall.sh, valerter.service)
- Updated Quick Start - Clear separation between .deb and static binary installation paths
1.0.0-rc.2 - 2026-01-15
Added
- Log timestamp in notifications - Original log timestamp now included in all channels (Mattermost footer, Email template, Webhook payload)
- Configurable timezone - New
timestamp_timezonesetting for formatted timestamps (default: UTC) - Cisco switches example - Complete alerting example for BPDU Guard violations in
examples/cisco-switches/ - Nginx proxy documentation - Required configuration for streaming endpoints (
proxy_buffering off)
Fixed
- VictoriaLogs streaming connection - Use HTTP GET instead of POST for
/select/logsql/tailendpoint - Email Outlook compatibility - Simplified template with better rendering across email clients
- Metric description -
valerter_victorialogs_upnow correctly documented as connection status
Changed
- Refactored config module - Split monolithic
config.rsinto focused submodules - Refactored notify module - Split monolithic
notify.rsinto focused submodules
1.0.0-rc.1 - 2026-01-14
Added
- Email body template system - HTML email templates with
body_htmlfield - Default email template - Built-in
templates/default-email.html.j2 - Fail-fast validation - Startup error if email destination uses template without
body_html - Modular documentation - New
docs/folder with detailed guides (getting-started, configuration, notifiers, metrics, architecture)
Fixed
- Metrics recorder race condition - Resolved startup race in Prometheus recorder initialization
Changed
- BREAKING:
color→accent_color- Template field renamed for clarity - BREAKING:
iconremoved - Template field no longer supported - README refactored - Reduced from 445 to ~110 lines, now a showcase with links to docs/
- Pipeline diagram - Mermaid replaced with static SVG (no overlay controls)
1.0.0-beta.1 - 2025-01-14
Added
valerter_build_infometric - Exposes version label for Prometheus dashboards- CHANGELOG.md - Document all changes following Keep a Changelog format
Fixed
- Debian package auto-restart - Service now automatically restarts on upgrade if running
Changed
- CI optimization - Skip CI for documentation and asset-only changes (
.md, images)
1.0.0-alpha.2 - 2025-01-14
Added
- 6 new Prometheus metrics for self-monitoring:
valerter_alerts_passed_total- alerts that passed throttlingvalerter_rule_panics_total- rule task panics (auto-restarted)valerter_rule_errors_total- fatal rule errorsvalerter_last_query_timestamp- timestamp of last successful queryvalerter_victorialogs_up- VictoriaLogs connection statusvalerter_query_duration_seconds- query latency histogram
- Official logo with SVG/PNG variants (light, dark, inverted, lockup)
- Example Prometheus alerts in README for monitoring valerter itself
- “Why Valerter?” section in README comparing with vmalert
Changed
- Simplified installation:
valerter_latest_amd64.debnow available via/releases/latest/download/ - README header with centered logo and badges (License, Rust version)
1.0.0-alpha.1 - 2025-01-12
Added
- Initial alpha release
- Real-time log streaming from VictoriaLogs
/tailAPI - Multi-channel notifications: Mattermost, Email SMTP, Generic Webhook
- Declarative YAML configuration with regex/JSON parsing
- Intelligent throttling with configurable rate limiting per key
- Prometheus metrics endpoint (
/metrics) - Debian package (.deb) and tarball releases
- systemd service integration