Files
threadnet-operating/monitoring/prometheus/alerts.yml
T
sorbandClaude Opus 4.8 e9c13dcf22 feat(monitoring): scrape alertmanager, alert on failed delivery; fix stale README
Alertmanager was configured as an alerting target but never scraped, so its own
metrics were absent: a silently breaking alert chain could not report itself —
the same blind spot as a missing series, now at the end of the chain. Adds the
operating_alertmanager scrape job and AlertDeliveryFailing on
alertmanager_notifications_failed_total.

The README still claimed alert delivery was deliberately muted via a
room=security null receiver. That route is gone; alertmanager.yml routes
everything to the matrix receiver, so the backup alerts added yesterday do get
delivered. Documentation asserting the opposite is dangerous in both directions,
so it now states the current wiring and keeps the alert-storm history as
background.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 12:00:00 +00:00

162 lines
8.2 KiB
YAML

# Alert-Regeln (gitops#32). Bewusst wenige, dafuer die real erlebten Fehlerklassen:
# - TargetDown: "Exporter/Dienst tot und keiner merkt es" (coturn-Klasse)
# - KubePodRestartLoop: Crashloops im k3s-Cluster (36k-coturn-Restarts-Klasse)
# - HostOom/HostLowMemory/HostLowDisk/HostHighSwap: Ressourcendruck der Hosts
groups:
- name: axion-basics
rules:
- alert: TargetDown
expr: up == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Target {{ $labels.job }}/{{ $labels.instance }} ist seit 5m nicht erreichbar"
- alert: KubePodRestartLoop
expr: increase(kube_pod_container_status_restarts_total[1h]) > 3
for: 10m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} restartet wiederholt ({{ $value | printf \"%.0f\" }}x in 1h)"
- alert: HostOomKill
expr: increase(node_vmstat_oom_kill[15m]) > 0
labels:
severity: warning
annotations:
summary: "OOM-Kill auf {{ $labels.instance }}"
- alert: HostLowMemory
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.08
for: 15m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }}: <8% RAM verfuegbar seit 15m"
- alert: HostHighSwap
expr: (node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes) / node_memory_SwapTotal_bytes > 0.5
for: 30m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }}: >50% Swap in Nutzung seit 30m"
- alert: HostLowDisk
expr: node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{mountpoint="/",fstype!~"tmpfs|overlay"} < 0.10
for: 15m
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }}: <10% Platz auf / frei"
# CVE-Pipeline (gitops#47): Funde aus den Trivy-Scans. Pflichtfelder je Alarm:
# CVE-ID, Mitigation (Fix-Version + NVD-Link), Ort (host), Typ (target_type);
# der Zeitstrahl kommt aus trivy_vuln_first_seen_timestamp + resolved-Edit.
# Alle Regeln routen per room-Label in den Security-Raum.
- name: axion-cve
rules:
# Aggregiert pro Image statt pro CVE (gitops#51, Entscheidung sorb
# 2026-08-01): ~1000 Einzelalarme waeren Rauschen; die menschlich
# handhabbare Einheit ist "Image X hat N kritische CVEs". Die
# CVE-Details (Pflichtfelder) liegen im Grafana-Dashboard cve-overview,
# die trivy_vuln_info-Einzelserien bleiben dafuer erhalten.
- alert: TrivyCriticalVulns
expr: count by (target, target_type, host) (trivy_vuln_info{severity="CRITICAL"}) > 0
labels:
severity: critical
room: security
annotations:
summary: "{{ $labels.target }} [{{ $labels.target_type }}@{{ $labels.host }}]: {{ $value }} CRITICAL-CVEs — Details: https://selendis.axion1337.de/d/cve-overview"
- alert: TrivyHighVulns
expr: count by (target, target_type, host) (trivy_vuln_info{severity="HIGH"}) > 0
for: 24h
labels:
severity: warning
room: security
annotations:
summary: "{{ $labels.target }} [{{ $labels.target_type }}@{{ $labels.host }}]: {{ $value }} HIGH-CVEs (seit 24h offen) — Details: https://selendis.axion1337.de/d/cve-overview"
- alert: TrivyScanStale
expr: time() - trivy_last_scan_timestamp > 172800
labels:
severity: warning
room: security
annotations:
summary: "CVE-Scan fuer {{ $labels.target }} ist aelter als 2 Tage — Scanner pruefen (ein still gestorbener Scanner macht blind)"
# Sicherungen (management #0030). Bis 2026-08-14 gab es hierzu KEINE Regel: ein
# fehlgeschlagenes naechtliches Backup waere unbemerkt geblieben - genau der stille
# Ausfall, den das Issue beschreibt. Metriken kommen aus kube-state-metrics im
# Matrix-Cluster (Alloy -> remote_write, kube_job_*/kube_cronjob_* passieren den
# Filter, der nur go_.*|process_.* verwirft).
- name: axion-backup
rules:
# Ein Backup- oder Probe-Job ist fehlgeschlagen.
- alert: BackupJobFailed
expr: max by (namespace, job_name) (kube_job_status_failed{job_name=~"(synapse|authentik|wikijs)-backup.*|restore-drill.*"}) > 0
for: 15m
labels:
severity: critical
annotations:
summary: "Backup-Job {{ $labels.namespace }}/{{ $labels.job_name }} ist fehlgeschlagen — ohne Sicherung ist der naechste Ausfall ein Datenverlust"
# Der CronJob plant gar nicht mehr (ausgesetzt, geloescht, Cluster-Problem).
# 26h Toleranz fuer die taeglichen Laeufe um 03:00/03:15/03:30.
# Fallback auf die Anlagezeit: ein frisch (neu) angelegter CronJob hat noch
# keine last_schedule_time-Serie - ein Ausdruck ueber eine fehlende Serie
# liefert nichts, der Alarm koennte also nie feuern. max by(...) fasst beide
# Metriken zu EINER Serie je CronJob zusammen; ohne das wuerde die nie
# aktualisierte created-Serie nach Ablauf der Frist dauerhaft falsch feuern
# ('or' matcht inkl. __name__, ergibt also eine Vereinigung beider Serien).
- alert: BackupNotRunning
expr: time() - max by (namespace, cronjob) (kube_cronjob_status_last_schedule_time{cronjob=~"(synapse|authentik|wikijs)-backup"} or kube_cronjob_created{cronjob=~"(synapse|authentik|wikijs)-backup"}) > 93600
labels:
severity: critical
annotations:
summary: "Seit ueber 26h kein Lauf von {{ $labels.cronjob }} — CronJob ausgesetzt oder verschwunden?"
# Die monatliche Restore-Probe laeuft nicht mehr. Eine Sicherung, die nie
# zurueckgespielt wurde, ist eine Vermutung — deshalb ist auch das Ausbleiben
# der PRUEFUNG ein Alarm, nicht nur ihr Fehlschlag. 40 Tage = Monatsrhythmus + Puffer.
# Bis zum ersten REGULAEREN Lauf gibt es keine last_schedule_time-Serie (ein
# manuell ausgeloester Job setzt sie nicht) - ohne Fallback waere dieser
# Alarm bis dahin blind. Gleiche max-by-Konstruktion wie oben.
- alert: RestoreDrillStale
expr: time() - max by (namespace, cronjob) (kube_cronjob_status_last_schedule_time{cronjob="restore-drill"} or kube_cronjob_created{cronjob="restore-drill"}) > 3456000
labels:
severity: warning
annotations:
summary: "Restore-Probe seit ueber 40 Tagen nicht gelaufen — Wiederherstellbarkeit ist wieder unbewiesen (Verfahren: notfallhandbuch)"
# Ein Backup-CronJob existiert gar nicht mehr (geloescht, Kustomization
# entfernt, Namespace weg). Bei den Alarmen oben verschwindet dann die
# Serie - und eine verschwundene Serie ist in Prometheus kein Alarm,
# sondern Stille. Genau das war der Fehler, den dieses Regelwerk
# verhindern soll, also ist das Fehlen selbst der Alarm.
# for: 30m puffert kurze Luecken der Metrik-Pipeline ab.
- alert: BackupCronJobMissing
expr: >-
absent(kube_cronjob_info{namespace="matrix", cronjob="synapse-backup"})
or absent(kube_cronjob_info{namespace="matrix", cronjob="wikijs-backup"})
or absent(kube_cronjob_info{namespace="authentik", cronjob="authentik-backup"})
or absent(kube_cronjob_info{namespace="matrix", cronjob="restore-drill"})
for: 30m
labels:
severity: critical
annotations:
summary: "Ein Backup-/Probe-CronJob fehlt im Cluster — Sicherung oder Wiederherstellungs-Nachweis laeuft nicht mehr"
# Die Alarmkette meldet ihr eigenes Reissen. Ironie inklusive: schlaegt die
# Zustellung komplett fehl, kommt auch dieser Alarm nicht an - er ist dann
# aber in Prometheus/Grafana sichtbar, statt dass gar nichts existiert.
# Teilausfaelle (ein Receiver von mehreren, Rate-Limit 429) meldet er sauber.
- alert: AlertDeliveryFailing
expr: increase(alertmanager_notifications_failed_total[15m]) > 0
labels:
severity: critical
annotations:
summary: "Alertmanager konnte Benachrichtigungen nicht zustellen ({{ $labels.integration }}) — Alarme laufen ins Leere"