Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.2.0] - unreleased

### Breaking changes
- **Template field `body_html` renamed to `email_body_html`** to reflect that
only the email notifier consumes it (Telegram, Mattermost, and webhook always
ignored it). Migration: in every template, replace `body_html:` with
`email_body_html:`. This applies to templates defined inline in `config.yaml`
*and* to any split files under `templates.d/`. Configs using the old name are
rejected at load time with a clear error that lists `email_body_html` among
the expected fields, so `valerter --validate` will point out every template
that needs updating on the first run.

### Fixed
- **Dotted field access in templates** (issue #25) — fields like
`server.hostname` or `http.request.method` can 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` (now `email_body_html`) is email-only so users do not
accidentally leave `body` empty.

## [1.1.0] - 2026-04-15

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "valerter"
version = "1.1.0"
version = "1.2.0"
edition = "2024"
description = "Real-time log alerting for VictoriaLogs"
license = "Apache-2.0"
Expand Down
42 changes: 28 additions & 14 deletions config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,24 +97,38 @@ defaults:
# └────────────────────────────────────────────────────────────────────────────┘
# Message templates using Jinja2 syntax (minijinja).
#
# Built-in variables:
# rule_name - Name of the triggered rule
# log_timestamp - Original log timestamp (ISO8601)
# log_timestamp_formatted - Human-readable timestamp (uses defaults.timestamp_timezone)
# A template entry has two sides:
#
# Parser variables:
# Any field extracted by the rule's parser (JSON fields or regex named groups)
# 1. Template variables AVAILABLE on the right-hand side of each line below.
# These come from the matched VictoriaLogs event and the runtime:
# _msg, _time, _stream VL built-in event fields
# (`_stream_id` is present only on
# streams configured server-side)
# <any parser-extracted field> JSON field or named regex group
# rule_name name of the triggered rule
# log_timestamp event timestamp (ISO8601)
# log_timestamp_formatted human timestamp (defaults.timestamp_timezone)
#
# Fields:
# title - Alert title (required)
# body - Alert body, plain text or Markdown (required)
# body_html - HTML version for email (optional, falls back to body)
# accent_color - Hex color for visual indicators (optional, e.g., "#ff0000")
# 2. Template OUTPUT KEYS that you define below (left-hand side). These are
# the slots valerter fills and hands off to notifiers:
# title Alert title (required)
# body Alert body, plain text or Markdown (required)
# email_body_html HTML-enriched version used ONLY by the email notifier
# (Telegram, Mattermost, and webhook all read `body` and
# ignore `email_body_html`). To format a Telegram alert, put
# `<b>`/`<i>`/`<code>` inline in `body` — `parse_mode: HTML`
# is Telegram's default.
# accent_color Hex color for visual indicators (optional, e.g., "#ff0000")
#
# Note: `title` and `body` are NOT variables you can reference on the right.
# If you want the raw log line, use `{{ _msg }}`.

templates:
default_alert:
title: "{{ title | default('Alert') }}"
body: "{{ body }}"
title: "{{ rule_name }}"
# _msg is the raw log line from VictoriaLogs — works on any event without
# needing a parser. Replace with a parsed field for richer templates.
body: "{{ _msg }}"
accent_color: "#3498db"

# ── Example: Severity-based template ───────────────────────────────────────
Expand All @@ -124,7 +138,7 @@ templates:
# body: |
# **Host:** {{ host }}
# **Message:** {{ message }}
# body_html: |
# email_body_html: |
# <h2 style="color: red;">{{ title }}</h2>
# <p><strong>Host:</strong> {{ host }}</p>
# <p>{{ message }}</p>
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ At startup, Valerter validates (in order):
3. **Template syntax** — All templates compile (minijinja)
4. **Notifier config** — URLs, credentials, env vars resolve correctly
5. **Destinations exist** — Rule destinations match notifier names in registry
6. **Email body_html** — Templates used with email destinations have `body_html`
6. **Email template body** — Templates used with email destinations have `email_body_html`
7. **Mattermost channel warning** — Warns if `mattermost_channel` set but no Mattermost notifier in destinations

If any validation fails, Valerter exits immediately with a clear error message.
Expand Down Expand Up @@ -211,7 +211,7 @@ tokio::spawn(async { process().unwrap(); }); // Silent crash
## Security

- **Config file:** `chmod 600` recommended (secrets in plaintext)
- **HTML escaping:** `body_html` templates auto-escape variables (XSS prevention)
- **HTML escaping:** `email_body_html` templates auto-escape variables (XSS prevention)
- **TLS verification:** Enabled by default (`tls.verify: true`)
- **No shell execution:** No user input ever reaches a shell

Expand Down
8 changes: 4 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ templates:
default_alert:
title: "{{ title | default('Alert') }}" # REQUIRED
body: "{{ body }}" # REQUIRED
body_html: "<p>{{ body }}</p>" # REQUIRED for email destinations
email_body_html: "<p>{{ body }}</p>" # REQUIRED for email destinations
accent_color: "#ff0000" # Optional: hex color
```

Expand All @@ -211,9 +211,9 @@ Variables come from the parser output plus built-in fields:
- Webhook `body_template`
- Mattermost footer (automatically includes `log_timestamp_formatted`)

### body_html Requirement
### email_body_html Requirement

**Important:** Templates used with email destinations MUST include `body_html`. Valerter validates this at startup and will fail if missing.
**Important:** Templates used with email destinations MUST include `email_body_html`. Valerter validates this at startup and will fail if missing.

## Rules

Expand Down Expand Up @@ -350,7 +350,7 @@ This checks:
- Template syntax
- Notifier configuration
- Rule destinations exist
- Email templates have `body_html`
- Email templates have `email_body_html`

## See Also

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ templates:
default_alert:
title: "{{ title | default('Alert') }}"
body: "{{ body }}"
body_html: "<p>{{ body }}</p>"
email_body_html: "<p>{{ body }}</p>"

rules:
- name: "error_alert"
Expand Down
25 changes: 22 additions & 3 deletions docs/notifiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ Valerter supports multiple notification channels. Configure them in the `notifie

The most flexible notifier - works with any HTTP API.

> **Note about `email_body_html`** — Webhook reads the outer template's `body`
> output key (and `title`, `rule_name`, `log_timestamp`, `log_timestamp_formatted`)
> inside its own `body_template`. It does **not** receive `email_body_html`;
> `email_body_html` is email-only. If your HTTP target needs HTML, put it in `body`
> at the outer template and reference `{{ body }}` from the webhook
> `body_template`.

### Configuration

```yaml
Expand Down Expand Up @@ -178,16 +185,16 @@ notifiers:
| `starttls` | 587 | STARTTLS upgrade (recommended) |
| `tls` | 465 | Direct TLS connection |

### body_html Requirement
### email_body_html Requirement

**Important:** When using email destinations, your message template MUST include `body_html`:
**Important:** When using email destinations, your message template MUST include `email_body_html`:

```yaml
templates:
my_template:
title: "{{ title }}"
body: "{{ body }}"
body_html: "<p>{{ body }}</p>" # REQUIRED for email
email_body_html: "<p>{{ body }}</p>" # REQUIRED for email
```

Valerter validates this at startup and will fail if missing.
Expand Down Expand Up @@ -218,6 +225,11 @@ notifiers:

Send alerts to Mattermost channels via incoming webhooks.

> **Note about `email_body_html`** — Mattermost reads the outer template's `body`
> output key, **not** `email_body_html`. `email_body_html` is email-only. Mattermost
> renders Markdown in `body` (`**bold**`, `*italic*`, fenced code blocks,
> lists, links); write your formatting there.

### Configuration

```yaml
Expand Down Expand Up @@ -265,6 +277,13 @@ This helps operators quickly locate the original log entry in VictoriaLogs.

Send alerts to one or more Telegram chats via the Bot API.

> **Note about `email_body_html`** — Telegram reads the outer template's `body`
> output key, **not** `email_body_html`. `email_body_html` is email-only. For rich
> formatting inside Telegram, put the markup directly in `body` using
> Telegram's supported HTML subset: `<b>`, `<i>`, `<u>`, `<s>`, `<code>`,
> `<pre>`, `<blockquote>`, `<a href="…">`, `<span>`, `<tg-spoiler>`.
> Keep `parse_mode: HTML` (the default) so the Bot API interprets those tags.

### Prerequisites

1. Create a bot via [@BotFather](https://t.me/BotFather) and note the token it gives you.
Expand Down
2 changes: 1 addition & 1 deletion examples/cisco-switches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ templates:
```
{{ _msg }}
```
body_html: |
email_body_html: |
<!-- HTML version for email -->
```

Expand Down
2 changes: 1 addition & 1 deletion examples/cisco-switches/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ templates:
```
{{ _msg }}
```
body_html: |
email_body_html: |
<div style="border-left: 4px solid #dc3545; padding-left: 16px; margin-bottom: 16px;">
<h2 style="color: #dc3545; margin: 0 0 8px 0;">🚨 BPDU Guard Violation</h2>
<p style="color: #6b7280; margin: 0;">A port has been disabled due to BPDU reception</p>
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-notifiers/config-test-notifiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ templates:
Alert triggered at {{ timestamp }}
Host: {{ host | default("unknown") }}
Message: {{ _msg | default("N/A") }}
body_html: |
email_body_html: |
<h2>Alert triggered at {{ timestamp }}</h2>
<p><strong>Host:</strong> {{ host | default("unknown") }}</p>
<p><strong>Message:</strong> {{ _msg | default("N/A") }}</p>
Expand Down
4 changes: 2 additions & 2 deletions src/config/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub struct CompiledThrottle {
pub struct CompiledTemplate {
pub title: String,
pub body: String,
pub body_html: Option<String>,
pub email_body_html: Option<String>,
pub accent_color: Option<String>,
}

Expand Down Expand Up @@ -146,7 +146,7 @@ impl Config {
CompiledTemplate {
title: template.title,
body: template.body,
body_html: template.body_html,
email_body_html: template.email_body_html,
accent_color: template.accent_color,
},
)
Expand Down
57 changes: 49 additions & 8 deletions src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ fn make_runtime_config_with_destinations(destinations: Vec<String>) -> RuntimeCo
CompiledTemplate {
title: "{{ title }}".to_string(),
body: "{{ body }}".to_string(),
body_html: None,
email_body_html: None,
accent_color: None,
},
);
Expand Down Expand Up @@ -451,7 +451,7 @@ fn validate_collects_all_errors() {
TemplateConfig {
title: "{{ title }}".to_string(),
body: "{{ body }}".to_string(),
body_html: None,
email_body_html: None,
accent_color: None,
},
);
Expand Down Expand Up @@ -528,7 +528,7 @@ fn validate_throttle_key_template() {
TemplateConfig {
title: "{{ title }}".to_string(),
body: "{{ body }}".to_string(),
body_html: None,
email_body_html: None,
accent_color: None,
},
);
Expand Down Expand Up @@ -591,7 +591,7 @@ fn validate_nonexistent_notify_template_fails() {
TemplateConfig {
title: "{{ title }}".to_string(),
body: "{{ body }}".to_string(),
body_html: None,
email_body_html: None,
accent_color: None,
},
);
Expand Down Expand Up @@ -710,7 +710,7 @@ fn load_config_with_unknown_notifier_type_fails() {
}

#[test]
fn validate_body_html_syntax_error_detected() {
fn validate_email_body_html_syntax_error_detected() {
let yaml = r#"
victorialogs:
url: http://localhost:9428
Expand All @@ -726,7 +726,7 @@ templates:
test:
title: "Test"
body: "Test body"
body_html: "{% if unclosed"
email_body_html: "{% if unclosed"
rules: []
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
Expand All @@ -736,13 +736,54 @@ rules: []
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| {
if let crate::error::ConfigError::InvalidTemplate { message, .. } = e {
message.contains("body_html")
message.contains("email_body_html")
} else {
false
}
}));
}

// Regression guard for the v1.2.0 rename of `body_html` → `email_body_html`.
// The old field name must be rejected at parse time with an error message that
// mentions both names, so users upgrading from 1.1.x get an actionable hint.
#[test]
fn parse_rejects_old_body_html_field_name() {
let yaml = r#"
victorialogs:
url: http://localhost:9428
notifiers:
test:
type: mattermost
webhook_url: "https://example.com/hooks/test"
defaults:
throttle:
count: 5
window: 1m
templates:
test:
title: "Test"
body: "Test body"
body_html: "<p>legacy</p>"
rules: []
"#;
let result: Result<Config, _> = serde_yaml::from_str(yaml);
assert!(
result.is_err(),
"YAML with legacy `body_html` field must be rejected at parse time"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("body_html"),
"Error should mention the legacy field name `body_html`, got: {}",
err_msg
);
assert!(
err_msg.contains("email_body_html"),
"Error should list the new field name `email_body_html` among expected fields, got: {}",
err_msg
);
}

#[test]
fn validate_config_with_invalid_accent_color_fails() {
let yaml = r#"
Expand Down Expand Up @@ -925,7 +966,7 @@ fn validate_rule_destinations_collects_all_errors() {
CompiledTemplate {
title: "{{ title }}".to_string(),
body: "{{ body }}".to_string(),
body_html: None,
email_body_html: None,
accent_color: None,
},
);
Expand Down
Loading
Loading