diff options
| author | Tim Keller <tjk@tjkeller.xyz> | 2026-08-27 23:22:24 -0500 |
|---|---|---|
| committer | Tim Keller <tjk@tjkeller.xyz> | 2026-08-27 23:22:24 -0500 |
| commit | fe770e9f386e091dbca7cf587cfe0131f10bd728 (patch) | |
| tree | a673fa706f1049c5fa43bb78fc3d970aebe28455 /rules | |
| parent | 89f2c328fc1b113a9e9b862841e0c7d0391b3d09 (diff) | |
| download | telemetry-flake-fe770e9f386e091dbca7cf587cfe0131f10bd728.tar.xz telemetry-flake-fe770e9f386e091dbca7cf587cfe0131f10bd728.zip | |
add server.alerts: Alertmanager with email notifications
Turns the Alertmanager wiring sketched out in conversation into an option, and
ships the alerting rules rather than leaving them as something to copy out of a
chat log.
services.telemetry.server.alerts = {
enable = true;
to = [ "you@example.com" ];
from = "alerts@example.com";
smarthost = "smtp.fastmail.com:587";
username = "alerts@example.com";
passwordFile = config.sops.secrets.smtp-password.path;
};
passwordFile becomes smtp_auth_password_file, read at runtime, so the password
never enters the store. `settings` is merged over the generated configuration
for inhibit rules and severity routing. Alerts group by alertname and host, so
nine failing drives on one machine arrive as one mail.
rules/alerts.yml carries twelve alerts: drive failing, degraded, failed
self-test, overheating; pool not ONLINE, unrepairable data errors, over 85%
full, scrub or snapshots overdue; exporter down, collector stale, textfile parse
error. Most are one-liners because rules/smartctl.yml has already reduced drive
health to a single smartctl:health_code. ExporterDown earns its place by
covering the case where a machine vanishes and every other alert goes quiet.
Alertmanager is defined inside the existing services.prometheus block: Nix will
not let one attrset both define services.prometheus wholesale and add a path
beneath it. Same shape as the grafana settings fix in 89f2c32.
Verified by building both ways. Enabled: two promtool-checked rule files, an
amtool-checked Alertmanager config with the password file, and Prometheus
pointing at 127.0.0.1:9093. Disabled: no alertmanager unit, one rule file, no
alerting stanza. The README's rule-override and alerts.settings snippets were
also built rather than written from memory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKczN1f66kmLxNBLX37voQ
Diffstat (limited to 'rules')
| -rw-r--r-- | rules/alerts.yml | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/rules/alerts.yml b/rules/alerts.yml new file mode 100644 index 0000000..cd3b1a7 --- /dev/null +++ b/rules/alerts.yml @@ -0,0 +1,140 @@ +# Alerting rules for the collectors' metrics. +# +# Loaded when services.telemetry.server.alerts.enable is on. Most of the conditions are +# one-liners because rules/smartctl.yml has already reduced drive health to a single +# smartctl:health_code per drive -- see that file for how the verdict is derived. +# +# Every alert carries the `host` label, so notifications say which machine. That label +# comes from the relabelling in modules/server.nix, not from the exporters. +# +# These are deliberately conservative: long `for` durations, and thresholds chosen so a +# healthy fleet is silent. If you want different numbers, set +# services.telemetry.server.alerts.defaultRules = false and point +# services.telemetry.server.prometheus.ruleFiles at your own copy. + +groups: + - name: drives + rules: + # smartctl:health_code is 2 when the drive's own SMART self-assessment failed, an + # attribute is at or below its vendor threshold, or the drive reported errors it + # could not correct. Any of those means replace it. + - alert: DriveFailing + expr: smartctl:health_code == 2 + for: 15m + labels: + severity: critical + annotations: + summary: "{{ $labels.host }}: drive {{ $labels.device }} is failing" + description: >- + SMART reports this drive as failing. Check the Drive Health dashboard for + which attribute tripped, and plan a replacement. + + # Reallocated, pending or uncorrectable sectors on SATA; a non-empty grown defect + # list on SAS. Not failing yet, so this is a long-fuse warning rather than a page. + - alert: DriveCaution + expr: smartctl:health_code == 1 + for: 6h + labels: + severity: warning + annotations: + summary: "{{ $labels.host }}: drive {{ $labels.device }} has degraded sectors" + description: >- + Watch the trend rather than the absolute number -- a stable count is + tolerable, growth is not. + + - alert: SelfTestFailed + expr: smart_selftest_last_passed == 0 + for: 1h + labels: + severity: critical + annotations: + summary: "{{ $labels.host }}: {{ $labels.device }} failed its {{ $labels.test_type }} self-test" + + # Sustained operation above ~55C measurably shortens drive life. + - alert: DriveOverheating + expr: smartctl_device_temperature{temperature_type="current"} > 55 + for: 30m + labels: + severity: warning + annotations: + summary: "{{ $labels.host }}: {{ $labels.device }} is at {{ $value }}C" + + - name: pools + rules: + - alert: PoolNotOnline + expr: zfs_pool_health_code != 0 + for: 5m + labels: + severity: critical + annotations: + summary: "{{ $labels.host }}: pool {{ $labels.pool }} is not ONLINE" + + # Errors ZFS could not repair from redundancy: data has been lost and the affected + # files need restoring from backup. + - alert: PoolDataErrors + expr: zfs_pool_data_errors > 0 + for: 5m + labels: + severity: critical + annotations: + summary: "{{ $labels.host }}: pool {{ $labels.pool }} has unrepairable data errors" + + # ZFS write performance degrades sharply past ~80%, and a pool near 100% becomes + # effectively read-only. 85% leaves room to react. + - alert: PoolNearlyFull + expr: zfs_pool_capacity_ratio > 0.85 + for: 1h + labels: + severity: warning + annotations: + summary: "{{ $labels.host }}: pool {{ $labels.pool }} is {{ $value | humanizePercentage }} full" + + - alert: ScrubOverdue + expr: time() - zfs_pool_last_scrub_end_timestamp_seconds > 45 * 86400 + for: 1h + labels: + severity: warning + annotations: + summary: "{{ $labels.host }}: pool {{ $labels.pool }} has not been scrubbed in 45 days" + + # Two intervals late means the auto-snapshot timer has missed at least one run. + - alert: SnapshotsOverdue + expr: >- + (time() - zfs_snapshot_newest_timestamp_seconds) + > on(label) group_left() (2 * zfs_snapshot_interval_seconds) + for: 1h + labels: + severity: warning + annotations: + summary: "{{ $labels.host }}: {{ $labels.label }} snapshots of {{ $labels.dataset }} are overdue" + + - name: collection + rules: + # Covers both exporters. Without this, every other alert here goes quiet when a + # machine disappears -- absence of data is not absence of a problem. + - alert: ExporterDown + expr: up == 0 + for: 10m + labels: + severity: critical + annotations: + summary: "{{ $labels.host }}: the {{ $labels.job }} exporter is not responding" + + # The ZFS collector runs every minute and the SMART one every 15, so a file older + # than 30 minutes means its timer is failing and the dashboards are showing stale + # numbers without saying so. + - alert: CollectorStale + expr: time() - node_textfile_mtime_seconds > 1800 + for: 15m + labels: + severity: warning + annotations: + summary: "{{ $labels.host }}: {{ $labels.file }} has not been refreshed" + + - alert: TextfileParseError + expr: node_textfile_scrape_error > 0 + for: 15m + labels: + severity: warning + annotations: + summary: "{{ $labels.host }}: a collector wrote a malformed .prom file" |
