Files
homelab-configs/observability/prometheus/alert_rules.yml
T
tommy 16489cabe5 observability: fix container memory alerts to use working set (C-02)
ContainerMemoryNearLimit and ContainerMemoryAtLimit divided
container_memory_usage_bytes (which includes reclaimable inactive_file
page cache) by the container limit. On prometheus this read ~99% and
fired AtLimit critical ("OOM kill expected shortly") despite anon
working set being only ~365 MiB and memory.events oom_kill = 0.

Both rules now use container_memory_working_set_bytes, the OOM-relevant
figure (usage minus reclaimable cache). Validated with promtool and
reloaded via SIGHUP; both rules read inactive (~33%).

This versions a fix that was previously live-only at
/home/tommy/observability/prometheus/ and would silently revert on the
next repo-based redeploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:56:07 -05:00

543 lines
23 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
groups:
# ──────────────────────────────────────────────
# 1. Node availability
# ──────────────────────────────────────────────
- name: node_availability
rules:
- alert: NodeDown
expr: up{job="node-exporter"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Node {{ $labels.instance }} is down"
description: "node-exporter has not been reachable for >2 minutes."
- alert: NodeHighLoad
expr: >
node_load15
/ count without(cpu, mode) (node_cpu_seconds_total{mode="idle"})
> 0.85
for: 15m
labels:
severity: warning
annotations:
summary: "High 15-min load on {{ $labels.instance }}"
description: "Load/core ratio is {{ printf \"%.2f\" $value }} (>0.85) for 15 minutes."
- alert: NodeHighLoadCritical
expr: >
node_load15
/ count without(cpu, mode) (node_cpu_seconds_total{mode="idle"})
> 1.0
for: 10m
labels:
severity: critical
annotations:
summary: "Critical load on {{ $labels.instance }}"
description: "Load/core ratio is {{ printf \"%.2f\" $value }} (>1.0) for 10 minutes."
# ──────────────────────────────────────────────
# 2. CPU
# ──────────────────────────────────────────────
- name: cpu
rules:
- alert: CPUHighWarning
expr: >
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
for: 15m
labels:
severity: warning
annotations:
summary: "High CPU on {{ $labels.instance }}"
description: "CPU usage is {{ printf \"%.1f\" $value }}% for 15 minutes."
- alert: CPUHighCritical
expr: >
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 98
for: 5m
labels:
severity: critical
annotations:
summary: "Critical CPU on {{ $labels.instance }}"
description: "CPU usage is {{ printf \"%.1f\" $value }}% for 5 minutes."
# ──────────────────────────────────────────────
# 3. Memory
# ──────────────────────────────────────────────
- name: memory
rules:
- alert: MemoryHighWarning
expr: >
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
for: 5m
labels:
severity: warning
annotations:
summary: "High memory on {{ $labels.instance }}"
description: "Memory usage is {{ printf \"%.1f\" $value }}% (>90%)."
- alert: MemoryHighCritical
expr: >
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 95
for: 2m
labels:
severity: critical
annotations:
summary: "Critical memory on {{ $labels.instance }}"
description: "Memory usage is {{ printf \"%.1f\" $value }}% (>95%)."
- alert: SwapHighWarning
expr: >
(node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes)
/ node_memory_SwapTotal_bytes * 100 > 50
and node_memory_SwapTotal_bytes > 0
for: 15m
labels:
severity: warning
annotations:
summary: "High swap usage on {{ $labels.instance }}"
description: "Swap usage is {{ printf \"%.1f\" $value }}% (>50%)."
# ──────────────────────────────────────────────
# 4. Disk / filesystem
# $value = % used (expr produces percentage directly)
# ──────────────────────────────────────────────
- name: disk
rules:
- alert: DiskSpaceWarning
expr: >
(1 - node_filesystem_avail_bytes{fstype!~"tmpfs|ramfs|squashfs|overlay|devtmpfs|efivarfs"}
/ node_filesystem_size_bytes{fstype!~"tmpfs|ramfs|squashfs|overlay|devtmpfs|efivarfs"})
* 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "Disk >85% on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "{{ printf \"%.1f\" $value }}% used on {{ $labels.mountpoint }} ({{ $labels.device }})."
- alert: DiskSpaceCritical
expr: >
(1 - node_filesystem_avail_bytes{fstype!~"tmpfs|ramfs|squashfs|overlay|devtmpfs|efivarfs"}
/ node_filesystem_size_bytes{fstype!~"tmpfs|ramfs|squashfs|overlay|devtmpfs|efivarfs"})
* 100 > 95
for: 2m
labels:
severity: critical
annotations:
summary: "Disk >95% on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "{{ printf \"%.1f\" $value }}% used — disk nearly full."
- alert: DiskFillPredicted24h
expr: >
predict_linear(
node_filesystem_avail_bytes{fstype!~"tmpfs|ramfs|squashfs|overlay|devtmpfs|efivarfs"}[6h],
86400
) < 0
for: 30m
labels:
severity: warning
annotations:
summary: "Disk fill predicted <24h on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "At current write rate {{ $labels.mountpoint }} will fill within 24 hours."
- alert: InodesWarning
expr: >
(1 - node_filesystem_files_free{fstype!~"tmpfs|ramfs|squashfs|overlay|devtmpfs"}
/ node_filesystem_files{fstype!~"tmpfs|ramfs|squashfs|overlay|devtmpfs"})
* 100 > 90
for: 5m
labels:
severity: warning
annotations:
summary: "Inodes >90% on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "{{ printf \"%.1f\" $value }}% of inodes used on {{ $labels.mountpoint }}."
- alert: EFIOrBootPartitionHighWarning
expr: >
(1 - node_filesystem_avail_bytes{mountpoint=~"/boot.*|/sys/firmware/efi.*"}
/ node_filesystem_size_bytes{mountpoint=~"/boot.*|/sys/firmware/efi.*"})
* 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "Boot/EFI partition >80% on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "{{ printf \"%.1f\" $value }}% used on {{ $labels.mountpoint }}. Clean old kernel/EFI entries."
- alert: EFIOrBootPartitionCritical
expr: >
(1 - node_filesystem_avail_bytes{mountpoint=~"/boot.*|/sys/firmware/efi.*"}
/ node_filesystem_size_bytes{mountpoint=~"/boot.*|/sys/firmware/efi.*"})
* 100 > 90
for: 2m
labels:
severity: critical
annotations:
summary: "Boot/EFI partition >90% on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "{{ printf \"%.1f\" $value }}% used. Risk of failed kernel installs or boot failures."
# ──────────────────────────────────────────────
# 5. ZFS pool health
# ──────────────────────────────────────────────
- name: zfs
rules:
- alert: ZFSPoolDegraded
expr: node_zfs_zpool_state{state="degraded"} == 1
for: 1m
labels:
severity: critical
annotations:
summary: "ZFS pool {{ $labels.zpool }} DEGRADED on {{ $labels.instance }}"
description: "Pool {{ $labels.zpool }} is DEGRADED. A disk has likely failed — check immediately."
- alert: ZFSPoolFaultedOrUnavail
expr: node_zfs_zpool_state{state=~"faulted|unavail|suspended"} == 1
for: 1m
labels:
severity: critical
annotations:
summary: "ZFS pool {{ $labels.zpool }} is {{ $labels.state }} on {{ $labels.instance }}"
description: "Pool {{ $labels.zpool }} is {{ $labels.state }}. Immediate attention required."
- alert: ZFSPoolOfflineOrRemoved
expr: node_zfs_zpool_state{state=~"offline|removed"} == 1
for: 2m
labels:
severity: warning
annotations:
summary: "ZFS pool {{ $labels.zpool }} is {{ $labels.state }} on {{ $labels.instance }}"
description: "Pool {{ $labels.zpool }} reported {{ $labels.state }}. Verify if intentional."
- alert: ZFSPoolCapacityWarning
expr: >
(1 - node_filesystem_avail_bytes{fstype="zfs"}
/ node_filesystem_size_bytes{fstype="zfs"})
* 100 > 85
for: 10m
labels:
severity: warning
annotations:
summary: "ZFS filesystem >85% on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "{{ printf \"%.1f\" $value }}% used. ZFS performance degrades above 80%."
- alert: ZFSPoolCapacityCritical
expr: >
(1 - node_filesystem_avail_bytes{fstype="zfs"}
/ node_filesystem_size_bytes{fstype="zfs"})
* 100 > 95
for: 5m
labels:
severity: critical
annotations:
summary: "ZFS filesystem >95% on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "{{ printf \"%.1f\" $value }}% used. ZFS writes will fail at 100%."
# ──────────────────────────────────────────────
# 6. SMART (dormant — requires smartctl-exporter)
# ──────────────────────────────────────────────
- name: smart
rules:
- alert: SMARTDeviceUnhealthy
expr: smartctl_device_smart_healthy == 0
for: 5m
labels:
severity: critical
annotations:
summary: "SMART failure on {{ $labels.instance }} device {{ $labels.device }}"
description: "SMART overall health check FAILED for {{ $labels.device }}."
- alert: SMARTReallocatedSectors
expr: >
smartctl_device_attribute{attribute_name="Reallocated_Sector_Ct",attribute_value_type="raw"}
> 0
for: 5m
labels:
severity: warning
annotations:
summary: "Reallocated sectors on {{ $labels.instance }} {{ $labels.device }}"
description: "{{ $value }} reallocated sectors — early sign of disk failure."
- alert: SMARTPendingSectors
expr: >
smartctl_device_attribute{attribute_name="Current_Pending_Sector",attribute_value_type="raw"}
> 0
for: 5m
labels:
severity: warning
annotations:
summary: "Pending sectors on {{ $labels.instance }} {{ $labels.device }}"
description: "{{ $value }} pending (unreadable) sectors. Disk may be failing."
# ──────────────────────────────────────────────
# 7. NFS mounts
# ──────────────────────────────────────────────
- name: nfs
rules:
- alert: NFSMountReadOnly
expr: node_filesystem_readonly{fstype=~"nfs.*", mountpoint=~"/mnt/.*"} == 1
for: 2m
labels:
severity: critical
annotations:
summary: "NFS mount read-only on {{ $labels.instance }} {{ $labels.mountpoint }}"
description: "NFS mount {{ $labels.mountpoint }} has gone read-only — possible server issue or network error."
- alert: MediaNFSMountMissing
expr: absent(node_filesystem_avail_bytes{mountpoint="/mnt/media", instance="192.168.99.183:9100"})
for: 3m
labels:
severity: critical
annotations:
summary: "Media NFS mount missing on media-server (.183)"
description: "/mnt/media (TrueNAS Tank) is not mounted. Plex/Immich will lose access to media."
- alert: SynoNFSMountMissing
expr: absent(node_filesystem_avail_bytes{mountpoint="/mnt/synology", instance="192.168.99.153:9100"})
for: 3m
labels:
severity: critical
annotations:
summary: "Synology NFS mount missing on PBS (.153)"
description: "/mnt/synology is not mounted on PBS. Synology-Remote backup datastore is offline."
# ──────────────────────────────────────────────
# 8. UPS / NUT
# hon95/prometheus-nut-exporter reports 01 scale.
# Expressions multiply to produce % so $value is human-readable.
# ──────────────────────────────────────────────
- name: ups
rules:
- alert: UPSBatteryLow
expr: nut_battery_charge * 100 < 30
for: 2m
labels:
severity: critical
annotations:
summary: "UPS {{ $labels.ups }} battery low ({{ printf \"%.0f\" $value }}%)"
description: "Battery at {{ printf \"%.0f\" $value }}%. Power loss may be imminent."
- alert: UPSBatteryWarning
expr: nut_battery_charge * 100 < 50
for: 5m
labels:
severity: warning
annotations:
summary: "UPS {{ $labels.ups }} battery below 50%"
description: "Battery at {{ printf \"%.0f\" $value }}%. May indicate on-battery or slow charging."
- alert: UPSLowRuntime
expr: nut_battery_runtime_seconds < 300
for: 2m
labels:
severity: critical
annotations:
summary: "UPS {{ $labels.ups }} runtime <5 minutes"
description: "Estimated runtime is {{ printf \"%.0f\" $value }}s. Systems may not shut down cleanly."
- alert: UPSHighLoad
expr: nut_load * 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "UPS {{ $labels.ups }} load >80%"
description: "UPS load is {{ printf \"%.0f\" $value }}%. Consider load balancing."
- alert: NUTExporterDown
expr: up{job="nut"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "NUT exporter is down"
description: "UPS monitoring is offline — cannot detect power events or battery state."
# ──────────────────────────────────────────────
# 9. TLS certificate expiry
# (dormant — requires blackbox_exporter or traefik metrics)
# ──────────────────────────────────────────────
- name: tls
rules:
- alert: TLSCertExpiryWarning
expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 30
for: 1h
labels:
severity: warning
annotations:
summary: "TLS cert expiring soon for {{ $labels.instance }}"
description: "Certificate expires in {{ printf \"%.0f\" $value | humanizeDuration }}."
- alert: TLSCertExpiryCritical
expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 7
for: 1h
labels:
severity: critical
annotations:
summary: "TLS cert expiring in <7 days for {{ $labels.instance }}"
description: "Certificate expires in {{ printf \"%.0f\" $value | humanizeDuration }}. Renewal may have failed."
- alert: TraefikCertExpiryWarning
expr: traefik_tls_certs_not_after - time() < 86400 * 30
for: 1h
labels:
severity: warning
annotations:
summary: "Traefik TLS cert expiring for {{ $labels.sans }}"
description: "Certificate expires in {{ printf \"%.0f\" $value | humanizeDuration }}."
# ──────────────────────────────────────────────
# 10. Container health
# ──────────────────────────────────────────────
- name: containers
rules:
- alert: ContainerOOM
expr: increase(container_oom_events_total{name!=""}[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "Container OOM kill: {{ $labels.name }}"
description: "Container {{ $labels.name }} was OOM-killed. Memory limit may need to be raised."
- alert: ContainerRestartLoop
expr: changes(container_start_time_seconds{name!=""}[1h]) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "Container restart loop: {{ $labels.name }}"
description: "Container {{ $labels.name }} has restarted {{ printf \"%.0f\" $value }} times in the last hour."
- alert: ContainerHighCPU
expr: rate(container_cpu_usage_seconds_total{name!="", id!="/"}[5m]) > 0.80
for: 15m
labels:
severity: warning
annotations:
summary: "High CPU in container {{ $labels.name }}"
description: "Container {{ $labels.name }} is using {{ printf \"%.2f\" $value }} CPU cores sustained for 15 minutes."
- alert: ContainerMemoryNearLimit
expr: >
(container_memory_working_set_bytes{name!=""}
/ container_spec_memory_limit_bytes{name!=""})
* 100 > 90
and container_spec_memory_limit_bytes{name!=""} > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.name }} memory >90% of limit"
description: "Using {{ printf \"%.1f\" $value }}% of memory limit. OOM risk."
- alert: ContainerMemoryAtLimit
expr: >
(container_memory_working_set_bytes{name!=""}
/ container_spec_memory_limit_bytes{name!=""})
* 100 > 98
and container_spec_memory_limit_bytes{name!=""} > 0
for: 2m
labels:
severity: critical
annotations:
summary: "Container {{ $labels.name }} memory at limit — OOM imminent"
description: "Using {{ printf \"%.1f\" $value }}% of memory limit. OOM kill expected shortly."
# ──────────────────────────────────────────────
# 11. Prometheus self-monitoring
# ──────────────────────────────────────────────
- name: prometheus_self
rules:
- alert: PrometheusConfigReloadFailed
expr: prometheus_config_last_reload_successful == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Prometheus config reload failed"
description: "Prometheus failed to reload its configuration. Alert rules may be stale."
- alert: AlertmanagerDown
expr: up{job="alertmanager"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Alertmanager is down"
description: "Prometheus cannot reach Alertmanager — no alerts will be delivered."
- alert: PrometheusTargetDown
expr: up == 0
for: 5m
labels:
severity: warning
annotations:
summary: "Prometheus target down: {{ $labels.job }}/{{ $labels.instance }}"
description: "Scrape target {{ $labels.instance }} (job={{ $labels.job }}) unreachable for >5 minutes."
- alert: AlertmanagerNotificationsFailing
expr: increase(alertmanager_notifications_failed_total[5m]) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Alertmanager notifications failing"
description: "Some alert notifications are failing to deliver. Check bridge logs."
# ──────────────────────────────────────────────
# 12. Media / Vault1 storage (TrueNAS Tank NFS)
# ──────────────────────────────────────────────
- name: media_storage
rules:
- alert: MediaStorageWarning
expr: >
(1 - node_filesystem_avail_bytes{mountpoint="/mnt/media", fstype=~"nfs.*"}
/ node_filesystem_size_bytes{mountpoint="/mnt/media", fstype=~"nfs.*"})
* 100 > 85
for: 10m
labels:
severity: warning
annotations:
summary: "Tank/Media share >85% full on {{ $labels.instance }}"
description: "{{ printf \"%.1f\" $value }}% used on the TrueNAS media share."
- alert: MediaStorageCritical
expr: >
(1 - node_filesystem_avail_bytes{mountpoint="/mnt/media", fstype=~"nfs.*"}
/ node_filesystem_size_bytes{mountpoint="/mnt/media", fstype=~"nfs.*"})
* 100 > 95
for: 5m
labels:
severity: critical
annotations:
summary: "Tank/Media share >95% full on {{ $labels.instance }}"
description: "{{ printf \"%.1f\" $value }}% used. Downloads/recordings will fail soon."
# ──────────────────────────────────────────────
# 13. UPS battery self-test results
# Metrics written by /usr/local/bin/ups-quarterly-test.sh via node-exporter textfile collector on Beast.
# 1=passed, 0=failed/unknown. Cron: first Sunday of Jan/Apr/Jul/Oct at 03:00.
# ──────────────────────────────────────────────
- name: ups_battery_test
rules:
- alert: UPSBatteryTestFailed
expr: ups_battery_test_result == 0
for: 5m
labels:
severity: warning
annotations:
summary: "UPS {{ $labels.ups }} quarterly battery test failed"
description: "Self-test result was not 'passed'. Run test manually: upscmd -u upsadmin cyberpower1@localhost test.battery.start.quick"
- alert: UPSBatteryTestStale
expr: time() - ups_battery_test_timestamp_seconds > 86400 * 100
for: 6h
labels:
severity: warning
annotations:
summary: "UPS {{ $labels.ups }} battery test overdue (>100 days)"
description: "Last self-test was {{ printf \"%.0f\" $value | humanizeDuration }} ago. Quarterly cron may have missed a run on Beast."