nix

configuration files that power my machines
Log | Files | Refs | README | LICENSE

commit 45d6b151b62c9698be48667c29666f2401db0130
parent f5cb5e7a59f1b1686beb45b9ed0f8f7c78f11b9c
Author: mtmn <miro@haravara.org>
Date:   Tue, 21 Apr 2026 01:12:55 +0200

feat: implement alertmanager/vmalert

Diffstat:
Mhosts/nixaran/configuration.nix | 0
Dmodules/mixins/corpus/grafana-dashboard.json | 519-------------------------------------------------------------------------------
Amodules/mixins/monitoring/alert-rules.yaml | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmodules/services/monitoring.nix | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msecrets.nix | 1+
Asecrets/smtp-alerts-password.age | 0
6 files changed, 293 insertions(+), 553 deletions(-)

diff --git a/hosts/nixaran/configuration.nix b/hosts/nixaran/configuration.nix Binary files differ. diff --git a/modules/mixins/corpus/grafana-dashboard.json b/modules/mixins/corpus/grafana-dashboard.json @@ -1,519 +0,0 @@ -{ - "title": "Corpus", - "uid": "corpus", - "version": 1, - "schemaVersion": 38, - "timezone": "browser", - "refresh": "30s", - "time": { "from": "now-24h", "to": "now" }, - "templating": { "list": [] }, - "annotations": { "list": [] }, - "panels": [ - { - "id": 1, - "type": "row", - "title": "HTTP", - "gridPos": { "x": 0, "y": 0, "w": 24, "h": 1 }, - "collapsed": false - }, - { - "id": 2, - "type": "timeseries", - "title": "Request Rate", - "gridPos": { "x": 0, "y": 1, "w": 12, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "sum by (method, path, status) (rate(corpus_http_requests_total[5m]))", - "legendFormat": "{{method}} {{path}} {{status}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "unit": "reqps", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 3, - "type": "timeseries", - "title": "Request Duration (p50 / p90 / p99)", - "gridPos": { "x": 12, "y": 1, "w": 12, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "histogram_quantile(0.50, sum by (le, path) (rate(corpus_http_request_duration_seconds_bucket[5m])))", - "legendFormat": "p50 {{path}}", - "refId": "A" - }, - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "histogram_quantile(0.90, sum by (le, path) (rate(corpus_http_request_duration_seconds_bucket[5m])))", - "legendFormat": "p90 {{path}}", - "refId": "B" - }, - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "histogram_quantile(0.99, sum by (le, path) (rate(corpus_http_request_duration_seconds_bucket[5m])))", - "legendFormat": "p99 {{path}}", - "refId": "C" - } - ], - "fieldConfig": { - "defaults": { - "unit": "s", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 10, - "type": "row", - "title": "Sync", - "gridPos": { "x": 0, "y": 9, "w": 24, "h": 1 }, - "collapsed": false - }, - { - "id": 11, - "type": "timeseries", - "title": "Sync Runs (per hour)", - "gridPos": { "x": 0, "y": 10, "w": 8, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "increase(corpus_sync_runs_total[1h])", - "legendFormat": "{{user}} {{source}} {{result}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 12, - "type": "timeseries", - "title": "Scrobbles Added (per hour)", - "gridPos": { "x": 8, "y": 10, "w": 8, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "increase(corpus_sync_scrobbles_added_total[1h])", - "legendFormat": "{{user}} {{source}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 13, - "type": "stat", - "title": "Time Since Last Sync", - "gridPos": { "x": 16, "y": 10, "w": 4, "h": 4 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "time() - corpus_sync_last_success_seconds", - "legendFormat": "{{user}} {{source}}", - "refId": "A", - "instant": true - } - ], - "fieldConfig": { - "defaults": { - "unit": "dtdurations", - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 3600 }, - { "color": "red", "value": 86400 } - ] - }, - "color": { "mode": "thresholds" } - }, - "overrides": [] - }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "orientation": "auto", - "textMode": "auto", - "colorMode": "value", - "graphMode": "none" - } - }, - { - "id": 14, - "type": "stat", - "title": "Time Since Last Backup", - "gridPos": { "x": 20, "y": 10, "w": 4, "h": 4 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "time() - corpus_db_backup_last_success_seconds", - "legendFormat": "{{user}}", - "refId": "A", - "instant": true - } - ], - "fieldConfig": { - "defaults": { - "unit": "dtdurations", - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 86400 }, - { "color": "red", "value": 172800 } - ] - }, - "color": { "mode": "thresholds" } - }, - "overrides": [] - }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "orientation": "auto", - "textMode": "auto", - "colorMode": "value", - "graphMode": "none" - } - }, - { - "id": 15, - "type": "stat", - "title": "Total Backup Runs", - "gridPos": { "x": 16, "y": 14, "w": 4, "h": 4 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "corpus_db_backup_runs_total", - "legendFormat": "{{user}}", - "refId": "A", - "instant": true - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "color": { "mode": "fixed", "fixedColor": "blue" } - }, - "overrides": [] - }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "orientation": "auto", - "textMode": "auto", - "colorMode": "value", - "graphMode": "none" - } - }, - { - "id": 16, - "type": "stat", - "title": "Total Sync Runs", - "gridPos": { "x": 20, "y": 14, "w": 4, "h": 4 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "sum by (source) (corpus_sync_runs_total)", - "legendFormat": "{{source}}", - "refId": "A", - "instant": true - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "color": { "mode": "fixed", "fixedColor": "blue" } - }, - "overrides": [] - }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "orientation": "auto", - "textMode": "auto", - "colorMode": "value", - "graphMode": "none" - } - }, - { - "id": 20, - "type": "row", - "title": "Enrichment", - "gridPos": { "x": 0, "y": 18, "w": 24, "h": 1 }, - "collapsed": false - }, - { - "id": 21, - "type": "timeseries", - "title": "Enrichment Fetches (per hour)", - "gridPos": { "x": 0, "y": 19, "w": 12, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "increase(corpus_enrichment_fetches_total[1h])", - "legendFormat": "{{user}} {{source}} {{result}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 22, - "type": "timeseries", - "title": "Enrichment Queue Size", - "gridPos": { "x": 12, "y": 19, "w": 12, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "corpus_enrichment_queue_size", - "legendFormat": "{{user}} {{type}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "min": 0, - "custom": { "lineWidth": 1, "fillOpacity": 10 }, - "thresholds": { - "mode": "absolute", - "steps": [ - { "color": "green", "value": null }, - { "color": "yellow", "value": 10 }, - { "color": "red", "value": 100 } - ] - } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 40, - "type": "row", - "title": "Cosine Club", - "gridPos": { "x": 0, "y": 36, "w": 24, "h": 1 }, - "collapsed": false - }, - { - "id": 41, - "type": "timeseries", - "title": "Cosine Requests (per hour)", - "gridPos": { "x": 0, "y": 37, "w": 12, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "increase(corpus_cosine_requests_total[1h])", - "legendFormat": "{{user}} {{result}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 42, - "type": "stat", - "title": "Cosine Requests by Result", - "gridPos": { "x": 12, "y": 37, "w": 12, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "sum by (result) (corpus_cosine_requests_total)", - "legendFormat": "{{result}}", - "refId": "A", - "instant": true - } - ], - "fieldConfig": { - "defaults": { - "unit": "short", - "color": { "mode": "fixed", "fixedColor": "blue" } - }, - "overrides": [] - }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "orientation": "auto", - "textMode": "auto", - "colorMode": "value", - "graphMode": "none" - } - }, - { - "id": 30, - "type": "row", - "title": "Runtime", - "gridPos": { "x": 0, "y": 45, "w": 24, "h": 1 }, - "collapsed": false - }, - { - "id": 31, - "type": "timeseries", - "title": "Event Loop Lag", - "gridPos": { "x": 0, "y": 46, "w": 8, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "nodejs_eventloop_lag_p50_seconds{job=\"corpus\"}", - "legendFormat": "p50", - "refId": "A" - }, - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "nodejs_eventloop_lag_p90_seconds{job=\"corpus\"}", - "legendFormat": "p90", - "refId": "B" - }, - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "nodejs_eventloop_lag_p99_seconds{job=\"corpus\"}", - "legendFormat": "p99", - "refId": "C" - } - ], - "fieldConfig": { - "defaults": { - "unit": "s", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 32, - "type": "timeseries", - "title": "Memory", - "gridPos": { "x": 8, "y": 46, "w": 8, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "process_resident_memory_bytes{job=\"corpus\"}", - "legendFormat": "RSS", - "refId": "A" - }, - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "nodejs_heap_size_used_bytes{job=\"corpus\"}", - "legendFormat": "Heap Used", - "refId": "B" - }, - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "nodejs_heap_size_total_bytes{job=\"corpus\"}", - "legendFormat": "Heap Total", - "refId": "C" - } - ], - "fieldConfig": { - "defaults": { - "unit": "bytes", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - }, - { - "id": 33, - "type": "timeseries", - "title": "GC Duration Rate", - "gridPos": { "x": 16, "y": 46, "w": 8, "h": 8 }, - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "targets": [ - { - "datasource": { "type": "prometheus", "uid": "DS_PROMETHEUS" }, - "expr": "rate(nodejs_gc_duration_seconds_sum{job=\"corpus\"}[5m])", - "legendFormat": "{{kind}}", - "refId": "A" - } - ], - "fieldConfig": { - "defaults": { - "unit": "s", - "custom": { "lineWidth": 1, "fillOpacity": 10 } - }, - "overrides": [] - }, - "options": { - "tooltip": { "mode": "multi", "sort": "none" }, - "legend": { "displayMode": "list", "placement": "bottom" } - } - } - ] -} diff --git a/modules/mixins/monitoring/alert-rules.yaml b/modules/mixins/monitoring/alert-rules.yaml @@ -0,0 +1,157 @@ +groups: + # ── Generic scrape target availability ───────────────────────────────────── + - name: targets + rules: + - alert: TargetDown + expr: up == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "Target {{ $labels.job }}/{{ $labels.instance }} is down" + description: > + Scrape target {{ $labels.instance }} (job: {{ $labels.job }}) has been + unreachable for more than 2 minutes. + + # ── Host-level metrics (node_exporter) ───────────────────────────────────── + - name: node + rules: + - alert: HighCPULoad + expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90 + for: 10m + labels: + severity: warning + annotations: + summary: "High CPU load: {{ $value | printf \"%.0f\" }}%" + description: "CPU usage has been above 90% for more than 10 minutes." + + - alert: HighMemoryUsage + expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90 + for: 5m + labels: + severity: warning + annotations: + summary: "High memory usage: {{ $value | printf \"%.0f\" }}%" + description: "Available memory has been below 10% for more than 5 minutes." + + - alert: HighDiskUsage + expr: > + (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|fuse.lxcfs|squashfs|ramfs|overlay"} + / node_filesystem_size_bytes) * 100 > 85 + for: 5m + labels: + severity: warning + annotations: + summary: "High disk usage on {{ $labels.mountpoint }}: {{ $value | printf \"%.0f\" }}%" + description: > + Disk usage on {{ $labels.mountpoint }} ({{ $labels.device }}) has been + above 85% for more than 5 minutes. + + - alert: DiskWillFillSoon + expr: > + predict_linear( + node_filesystem_avail_bytes{fstype!~"tmpfs|fuse.lxcfs|squashfs|ramfs|overlay"}[6h], + 4 * 3600 + ) < 0 + for: 30m + labels: + severity: warning + annotations: + summary: "Disk {{ $labels.mountpoint }} predicted to fill within 4 hours" + description: > + Based on the last 6 hours of growth, {{ $labels.mountpoint }} will run + out of space within 4 hours. + + - alert: HighSystemLoad + expr: node_load15 / count without(cpu, mode) (node_cpu_seconds_total{mode="idle"}) > 2 + for: 15m + labels: + severity: warning + annotations: + summary: "High 15-minute load average: {{ $value | printf \"%.2f\" }}" + description: "The 15-minute load average per CPU core has been above 2 for 15 minutes." + + # ── Systemd unit health (systemd_exporter) ───────────────────────────────── + - name: systemd + rules: + - alert: SystemdUnitFailed + expr: node_systemd_unit_state{state="failed"} == 1 + for: 5m + labels: + severity: critical + annotations: + summary: "Systemd unit {{ $labels.name }} failed" + description: > + The systemd unit {{ $labels.name }} has been in a failed state + for more than 5 minutes. + + # ── Container resources (cAdvisor) ───────────────────────────────────────── + - name: containers + rules: + - alert: ContainerHighCPU + expr: sum(rate(container_cpu_usage_seconds_total{name!=""}[5m])) by (name) * 100 > 80 + for: 10m + labels: + severity: warning + annotations: + summary: "Container {{ $labels.name }} CPU usage: {{ $value | printf \"%.0f\" }}%" + description: "Container {{ $labels.name }} CPU usage has been above 80% for 10 minutes." + + - alert: ContainerHighMemory + expr: > + ( + container_memory_usage_bytes{name!=""} + / container_spec_memory_limit_bytes{name!=""} + ) * 100 > 80 + unless container_spec_memory_limit_bytes{name!=""} == 0 + for: 5m + labels: + severity: warning + annotations: + summary: "Container {{ $labels.name }} memory: {{ $value | printf \"%.0f\" }}% of limit" + description: "Container {{ $labels.name }} memory usage has been above 80% of its limit for 5 minutes." + + - alert: ContainerOOMKilled + expr: increase(container_oom_events_total{name!=""}[5m]) > 0 + for: 1m + labels: + severity: critical + annotations: + summary: "Container {{ $labels.name }} was OOM-killed" + description: "Container {{ $labels.name }} was killed due to out-of-memory in the last 5 minutes." + + # ── SeaweedFS cluster (metrics on ports 9324–9327) ───────────────────────── + - name: seaweedfs + rules: + - alert: SeaweedFSDown + expr: up{job="seaweedfs"} == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "SeaweedFS component {{ $labels.instance }} is down" + description: "SeaweedFS at {{ $labels.instance }} has been unreachable for more than 2 minutes." + + # ── VictoriaMetrics self-health ───────────────────────────────────────────── + - name: victoriametrics + rules: + - alert: HighScrapeErrorRate + expr: rate(vm_promscrape_scrapes_failed_total[5m]) > 0.1 + for: 10m + labels: + severity: warning + annotations: + summary: "VictoriaMetrics scrape errors: {{ $value | printf \"%.2f\" }}/s" + description: "More than 10% of scrapes have been failing for the last 10 minutes." + + # ── vmalert self-health ───────────────────────────────────────────────────── + - name: vmalert + rules: + - alert: VMAlertRuleErrors + expr: increase(vmalert_execution_errors_total[5m]) > 0 + for: 10m + labels: + severity: warning + annotations: + summary: "vmalert rule evaluation errors" + description: "vmalert has been encountering rule evaluation errors. Check vmalert logs." diff --git a/modules/services/monitoring.nix b/modules/services/monitoring.nix @@ -6,6 +6,12 @@ }: let cfg = config.services.haravara.monitoring; + authSnippet = lib.optionalString (cfg.authLabel != null) "import ${ + if cfg.authLabel == "default" + then "auth" + else "auth_${cfg.authLabel}" + }"; + fetchDashboard = { name, sha256, @@ -29,6 +35,11 @@ sha256 = "665bc3e86b79749b2fdae7787f4b7502e00830898bf48f73c68134471f72bf33"; } { + name = "alertmanager.json"; + url = "https://grafana.com/api/dashboards/9578/revisions/4/download"; + sha256 = "16jvazvjswiyn281z4r7wmwb18841d3ar24951538kma28l05izy"; + } + { name = "seaweedfs.json"; url = "https://raw.githubusercontent.com/chrislusf/seaweedfs/c9100a7213e27963f58a0ba783c9598a177384b6/other/metrics/grafana_seaweedfs.json"; sha256 = "e364d113fed54127cbda10c4c2809d99a90dcc38bcd949b91846d0831e1a68e4"; @@ -52,10 +63,11 @@ ''; } ] - ++ lib.optional config.services.haravara.corpus.enable { + ++ lib.optional config.services.haravara.corpus.enable (fetchDashboard { name = "corpus.json"; - path = ../mixins/corpus/grafana-dashboard.json; - }); + url = "https://raw.githubusercontent.com/mtmn/corpus/master/monitoring/grafana-dashboard.json"; + sha256 = "0b6d4kvcwarv1isy3i4x0l4an7b3zyxyy9af3acr9gbfkkrbyjv5"; + })); in { options.services.haravara.monitoring = { enable = lib.mkEnableOption "Monitoring stack (VictoriaMetrics + Grafana)"; @@ -70,6 +82,50 @@ in { default = null; description = "The auth snippet to import"; }; + + alertmanager = { + enable = lib.mkEnableOption "Alertmanager for alert routing and notifications"; + domain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Domain to expose the Alertmanager UI via Caddy (optional)"; + }; + port = lib.mkOption { + type = lib.types.port; + default = 9093; + }; + configuration = lib.mkOption { + type = lib.types.attrs; + description = "Alertmanager configuration (route, receivers, etc.)"; + example = lib.literalExpression '' + { + route = { + receiver = "default"; + group_wait = "30s"; + group_interval = "5m"; + repeat_interval = "4h"; + }; + receivers = [{ + name = "default"; + webhook_configs = [{ url = "https://example.com/hook"; }]; + }]; + } + ''; + }; + }; + + vmalert = { + enable = lib.mkEnableOption "vmalert for Prometheus-compatible alert rule evaluation"; + port = lib.mkOption { + type = lib.types.port; + default = 8880; + }; + rules = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = []; + description = "Paths to YAML files containing alert/recording rules"; + }; + }; }; config = lib.mkIf cfg.enable { @@ -87,6 +143,29 @@ in { systemd = { services = { + vmalert = lib.mkIf cfg.vmalert.enable { + description = "VictoriaMetrics alert rule evaluator"; + wantedBy = ["multi-user.target"]; + after = ["network.target" "victoriametrics.service"]; + serviceConfig = { + ExecStart = lib.concatStringsSep " " ( + [ + "${pkgs.victoriametrics}/bin/vmalert" + "-datasource.url=http://127.0.0.1:8428" + "-remoteWrite.url=http://127.0.0.1:8428" + "-httpListenAddr=127.0.0.1:${toString cfg.vmalert.port}" + "-evaluationInterval=30s" + ] + ++ lib.optional cfg.alertmanager.enable + "-notifier.url=http://127.0.0.1:${toString cfg.alertmanager.port}" + ++ map (r: "-rule=${r}") cfg.vmalert.rules + ); + DynamicUser = true; + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + victoriametrics = { serviceConfig = { User = "victoriametrics"; @@ -122,6 +201,9 @@ in { victoriametrics = { enable = true; listenAddress = "127.0.0.1:8428"; + extraOptions = + lib.optional cfg.vmalert.enable + "-vmalert.proxyURL=http://127.0.0.1:${toString cfg.vmalert.port}"; prometheusConfig = { global.scrape_interval = "30s"; scrape_configs = @@ -162,29 +244,49 @@ in { job_name = "caddy"; static_configs = [{targets = ["127.0.0.1:2019"];}]; } + { + job_name = "loki"; + static_configs = [{targets = ["127.0.0.1:3100"];}]; + } ] ++ lib.optional config.services.haravara.corpus.enable { job_name = "corpus"; static_configs = [{targets = ["127.0.0.1:${toString config.services.haravara.corpus.port}"];}]; metrics_path = "/metrics"; + } + ++ lib.optional cfg.vmalert.enable { + job_name = "vmalert"; + static_configs = [{targets = ["127.0.0.1:${toString cfg.vmalert.port}"];}]; + } + ++ lib.optional cfg.alertmanager.enable { + job_name = "alertmanager"; + static_configs = [{targets = ["127.0.0.1:${toString cfg.alertmanager.port}"];}]; }; }; }; - prometheus.exporters.node = { - enable = true; - listenAddress = "127.0.0.1"; - enabledCollectors = ["systemd"]; - port = 9100; - }; + prometheus = { + exporters.node = { + enable = true; + listenAddress = "127.0.0.1"; + enabledCollectors = ["systemd"]; + port = 9100; + }; - prometheus.exporters.systemd = { - enable = true; - listenAddress = "127.0.0.1"; - port = 9558; - extraFlags = [ - "--systemd.collector.unit-exclude=.*\\\\.(mount|slice|scope|target|device|path|timer|automount|socket|swap)" - ]; + exporters.systemd = { + enable = true; + listenAddress = "127.0.0.1"; + port = 9558; + extraFlags = [ + "--systemd.collector.unit-exclude=.*\\\\.(mount|slice|scope|target|device|path|timer|automount|socket|swap)" + ]; + }; + + alertmanager = lib.mkIf cfg.alertmanager.enable { + enable = true; + listenAddress = "127.0.0.1"; + inherit (cfg.alertmanager) port configuration; + }; }; cadvisor = { @@ -320,24 +422,23 @@ in { }; }; - caddy.virtualHosts = { - "${cfg.grafanaDomain}".extraConfig = '' - ${lib.optionalString (cfg.authLabel != null) "import ${ - if cfg.authLabel == "default" - then "auth" - else "auth_${cfg.authLabel}" - }"} - reverse_proxy 127.0.0.1:3001 - ''; - "${cfg.victoriaDomain}".extraConfig = '' - ${lib.optionalString (cfg.authLabel != null) "import ${ - if cfg.authLabel == "default" - then "auth" - else "auth_${cfg.authLabel}" - }"} - reverse_proxy 127.0.0.1:8428 - ''; - }; + caddy.virtualHosts = + { + "${cfg.grafanaDomain}".extraConfig = '' + ${authSnippet} + reverse_proxy 127.0.0.1:3001 + ''; + "${cfg.victoriaDomain}".extraConfig = '' + ${authSnippet} + reverse_proxy 127.0.0.1:8428 + ''; + } + // lib.optionalAttrs (cfg.alertmanager.enable && cfg.alertmanager.domain != null) { + "${cfg.alertmanager.domain}".extraConfig = '' + ${authSnippet} + reverse_proxy 127.0.0.1:${toString cfg.alertmanager.port} + ''; + }; }; }; } diff --git a/secrets.nix b/secrets.nix @@ -16,4 +16,5 @@ in { "secrets/listenbrainz-token.age".publicKeys = allKeys; "secrets/mpd-host.age".publicKeys = allKeys; "secrets/corpus.age".publicKeys = allKeys; + "secrets/smtp-alerts-password.age".publicKeys = allKeys; } diff --git a/secrets/smtp-alerts-password.age b/secrets/smtp-alerts-password.age Binary files differ.