fix(grafana): split NVMe temperature alerts from HDD tiers
NVMe drives run hotter than HDDs and kept firing the 48°C HDD alerts. HDD tiers now exclude nvme devices; NVMe gets its own 65/70/75°C ladder. The device matcher tolerates both "nvme0" and "/dev/nvme0" label forms (smartctl_exporter strips the /dev/ prefix). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5efc0b0d6e
commit
1d37313bd6
+18
-1
@@ -191,7 +191,7 @@ Alerting](https://grafana.com/docs/grafana/latest/alerting/) as code under
|
|||||||
- `contact-points.yaml` — stock **webhook** receiver pointing at an ntfy topic,
|
- `contact-points.yaml` — stock **webhook** receiver pointing at an ntfy topic,
|
||||||
token sent as `Authorization: Bearer`. Deployed `0600` (secret).
|
token sent as `Authorization: Bearer`. Deployed `0600` (secret).
|
||||||
- `policies.yaml` — notification policy tree (root route to that receiver).
|
- `policies.yaml` — notification policy tree (root route to that receiver).
|
||||||
- `rules.yaml` — disk alert rules: HDD temperature
|
- `rules.yaml` — disk alert rules: HDD and NVMe temperature
|
||||||
(`smartctl_device_temperature{temperature_type="current"}`) and capacity
|
(`smartctl_device_temperature{temperature_type="current"}`) and capacity
|
||||||
(ZFS pools `zfs_pool_capacity_percent`; local filesystems
|
(ZFS pools `zfs_pool_capacity_percent`; local filesystems
|
||||||
`node_filesystem_avail_bytes` / `node_filesystem_size_bytes`, filtered to
|
`node_filesystem_avail_bytes` / `node_filesystem_size_bytes`, filtered to
|
||||||
@@ -206,6 +206,23 @@ entries to `rules.yaml`; the receiver and policy stay generic.
|
|||||||
> Provisioning the notification policy tree replaces the entire tree; UI edits
|
> Provisioning the notification policy tree replaces the entire tree; UI edits
|
||||||
> to policies are overwritten on the next deploy.
|
> to policies are overwritten on the next deploy.
|
||||||
|
|
||||||
|
### Temperature tiers
|
||||||
|
|
||||||
|
HDD and NVMe have different safe ranges, so they use separate ladders selected
|
||||||
|
by the `device` label (`nvme*` vs everything else). Cumulative thresholds, same
|
||||||
|
rise/fall semantics as capacity.
|
||||||
|
|
||||||
|
| Device | Threshold | `severity` | `for` |
|
||||||
|
|--------|-----------|-----------|-------|
|
||||||
|
| HDD | ≥48 / ≥50 / ≥52 / ≥54 / ≥56 / ≥58 / ≥60°C | warning → critical | 10m (5m at 60) |
|
||||||
|
| HDD | <20°C | warning | 10m |
|
||||||
|
| NVMe | ≥65°C | warning | 15m |
|
||||||
|
| NVMe | ≥70°C | high | 10m |
|
||||||
|
| NVMe | ≥75°C | critical | 5m |
|
||||||
|
|
||||||
|
NVMe runs hotter: 40-60°C is normal, 60-70°C is warm (acceptable under
|
||||||
|
sustained load), throttling typically starts at 70-80°C.
|
||||||
|
|
||||||
### Capacity tiers
|
### Capacity tiers
|
||||||
|
|
||||||
Cumulative thresholds: a rise fires each newly crossed threshold (lower tiers
|
Cumulative thresholds: a rise fires each newly crossed threshold (lower tiers
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ apiVersion: 1
|
|||||||
# capacity (ZFS pools + local filesystems).
|
# capacity (ZFS pools + local filesystems).
|
||||||
#
|
#
|
||||||
# Temperature metric: smartctl_device_temperature{temperature_type="current"} (job="smartctl").
|
# Temperature metric: smartctl_device_temperature{temperature_type="current"} (job="smartctl").
|
||||||
|
# Split by device class: HDDs (sd*) and NVMe (nvme*) have different
|
||||||
|
# safe ranges, so they get separate tier ladders.
|
||||||
|
# HDD: 48..60°C (below 45 ideal) — device!~"(/dev/)?nvme.*"
|
||||||
|
# NVMe: 65..75°C (40-60 normal, 60-70 warm, >70 throttling) — device=~"(/dev/)?nvme.*"
|
||||||
# Capacity metrics: zfs_pool_capacity_percent and
|
# Capacity metrics: zfs_pool_capacity_percent and
|
||||||
# node_filesystem_avail_bytes/node_filesystem_size_bytes (job="node").
|
# node_filesystem_avail_bytes/node_filesystem_size_bytes (job="node").
|
||||||
# node_filesystem_* is filtered to real local filesystems (ext4/xfs/btrfs);
|
# node_filesystem_* is filtered to real local filesystems (ext4/xfs/btrfs);
|
||||||
@@ -32,7 +36,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} >= 48
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} >= 48
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -80,7 +84,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} >= 50
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} >= 50
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -128,7 +132,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} >= 52
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} >= 52
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -176,7 +180,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} >= 54
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} >= 54
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -224,7 +228,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} >= 56
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} >= 56
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -272,7 +276,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} >= 58
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} >= 58
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -320,7 +324,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} >= 60
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} >= 60
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -368,7 +372,7 @@ groups:
|
|||||||
model:
|
model:
|
||||||
editorMode: code
|
editorMode: code
|
||||||
expr: >-
|
expr: >-
|
||||||
smartctl_device_temperature{job="smartctl", temperature_type="current"} < 20
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device!~"(/dev/)?nvme.*"} < 20
|
||||||
instant: false
|
instant: false
|
||||||
intervalMs: 1000
|
intervalMs: 1000
|
||||||
legendFormat: "{{device}}"
|
legendFormat: "{{device}}"
|
||||||
@@ -406,6 +410,154 @@ groups:
|
|||||||
summary: "Disk {{ $labels.device }} at {{ $values.B }}°C ({{ $labels.instance }})"
|
summary: "Disk {{ $labels.device }} at {{ $values.B }}°C ({{ $labels.instance }})"
|
||||||
description: "S.M.A.R.T. temperature is below the 20°C ideal minimum (sensor/fan/offline disk?)."
|
description: "S.M.A.R.T. temperature is below the 20°C ideal minimum (sensor/fan/offline disk?)."
|
||||||
|
|
||||||
|
# --- NVMe temperature tiers (device=~"(/dev/)?nvme.*") ---
|
||||||
|
# NVMe runs hotter than HDD: 40-60°C is normal, 60-70°C is warm but
|
||||||
|
# acceptable under sustained load, and throttling typically starts in the
|
||||||
|
# 70-80°C range. The HDD ladder above excludes NVMe devices.
|
||||||
|
- uid: nvme_temp_ge_65
|
||||||
|
title: "NVMe temperature ≥65°C"
|
||||||
|
condition: C
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange: { from: 3600, to: 0 }
|
||||||
|
datasourceUid: prometheus
|
||||||
|
model:
|
||||||
|
editorMode: code
|
||||||
|
expr: >-
|
||||||
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device=~"(/dev/)?nvme.*"} >= 65
|
||||||
|
instant: false
|
||||||
|
intervalMs: 1000
|
||||||
|
legendFormat: "{{device}}"
|
||||||
|
maxDataPoints: 43200
|
||||||
|
range: true
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
datasource: { type: __expr__, uid: __expr__ }
|
||||||
|
expression: A
|
||||||
|
reducer: last
|
||||||
|
refId: B
|
||||||
|
type: reduce
|
||||||
|
settings: { mode: dropNN }
|
||||||
|
- refId: C
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator: { params: [0], type: gt }
|
||||||
|
operator: { type: and }
|
||||||
|
query: { params: [C] }
|
||||||
|
reducer: { type: last }
|
||||||
|
type: query
|
||||||
|
datasource: { type: __expr__, uid: __expr__ }
|
||||||
|
expression: B
|
||||||
|
refId: C
|
||||||
|
type: threshold
|
||||||
|
for: 15m
|
||||||
|
noDataState: OK
|
||||||
|
execErrState: Error
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "NVMe {{ $labels.device }} at {{ $values.B }}°C ({{ $labels.instance }})"
|
||||||
|
description: "NVMe temperature is at or above 65°C (warm). Normal is 40-60°C; acceptable under sustained load."
|
||||||
|
|
||||||
|
- uid: nvme_temp_ge_70
|
||||||
|
title: "NVMe temperature ≥70°C"
|
||||||
|
condition: C
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange: { from: 3600, to: 0 }
|
||||||
|
datasourceUid: prometheus
|
||||||
|
model:
|
||||||
|
editorMode: code
|
||||||
|
expr: >-
|
||||||
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device=~"(/dev/)?nvme.*"} >= 70
|
||||||
|
instant: false
|
||||||
|
intervalMs: 1000
|
||||||
|
legendFormat: "{{device}}"
|
||||||
|
maxDataPoints: 43200
|
||||||
|
range: true
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
datasource: { type: __expr__, uid: __expr__ }
|
||||||
|
expression: A
|
||||||
|
reducer: last
|
||||||
|
refId: B
|
||||||
|
type: reduce
|
||||||
|
settings: { mode: dropNN }
|
||||||
|
- refId: C
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator: { params: [0], type: gt }
|
||||||
|
operator: { type: and }
|
||||||
|
query: { params: [C] }
|
||||||
|
reducer: { type: last }
|
||||||
|
type: query
|
||||||
|
datasource: { type: __expr__, uid: __expr__ }
|
||||||
|
expression: B
|
||||||
|
refId: C
|
||||||
|
type: threshold
|
||||||
|
for: 10m
|
||||||
|
noDataState: OK
|
||||||
|
execErrState: Error
|
||||||
|
labels:
|
||||||
|
severity: high
|
||||||
|
annotations:
|
||||||
|
summary: "NVMe {{ $labels.device }} at {{ $values.B }}°C ({{ $labels.instance }})"
|
||||||
|
description: "NVMe temperature is at or above 70°C. Thermal throttling typically begins in the 70-80°C range."
|
||||||
|
|
||||||
|
- uid: nvme_temp_ge_75
|
||||||
|
title: "NVMe temperature ≥75°C"
|
||||||
|
condition: C
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
relativeTimeRange: { from: 3600, to: 0 }
|
||||||
|
datasourceUid: prometheus
|
||||||
|
model:
|
||||||
|
editorMode: code
|
||||||
|
expr: >-
|
||||||
|
smartctl_device_temperature{job="smartctl", temperature_type="current", device=~"(/dev/)?nvme.*"} >= 75
|
||||||
|
instant: false
|
||||||
|
intervalMs: 1000
|
||||||
|
legendFormat: "{{device}}"
|
||||||
|
maxDataPoints: 43200
|
||||||
|
range: true
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
datasource: { type: __expr__, uid: __expr__ }
|
||||||
|
expression: A
|
||||||
|
reducer: last
|
||||||
|
refId: B
|
||||||
|
type: reduce
|
||||||
|
settings: { mode: dropNN }
|
||||||
|
- refId: C
|
||||||
|
datasourceUid: __expr__
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator: { params: [0], type: gt }
|
||||||
|
operator: { type: and }
|
||||||
|
query: { params: [C] }
|
||||||
|
reducer: { type: last }
|
||||||
|
type: query
|
||||||
|
datasource: { type: __expr__, uid: __expr__ }
|
||||||
|
expression: B
|
||||||
|
refId: C
|
||||||
|
type: threshold
|
||||||
|
for: 5m
|
||||||
|
noDataState: OK
|
||||||
|
execErrState: Error
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "NVMe {{ $labels.device }} at {{ $values.B }}°C ({{ $labels.instance }})"
|
||||||
|
description: "NVMe temperature is at or above 75°C (hot). Check airflow/heatsink; sustained throttling expected."
|
||||||
|
|
||||||
# --- Capacity: ZFS pools (data disk) ---
|
# --- Capacity: ZFS pools (data disk) ---
|
||||||
# zfs_pool_capacity_percent is emitted hourly by the zfs role's
|
# zfs_pool_capacity_percent is emitted hourly by the zfs role's
|
||||||
# zfs-metrics.timer via node_exporter's textfile collector. Cumulative
|
# zfs-metrics.timer via node_exporter's textfile collector. Cumulative
|
||||||
|
|||||||
Reference in New Issue
Block a user