Skip to content

Commit 43b24b1

Browse files
Samuel Warkentinclaude
andcommitted
feat(node-monitor): configurable thresholds + flap suppression
Node memory/disk/load checks hardcoded warn=80%/crit=90% and reported status on every single sample, so a transient spike (e.g. a backup job briefly pushing memory over the line) flapped the check ok<->critical repeatedly. Observed on k3s-node01 (base ~20%, brief 91% spikes -> 12 status transitions in one window) and a 4GB Pi sitting right at the warning line. - Expose MEM/DISK/LOAD warn+crit thresholds as env (chart values). - Add FLAP_SAMPLES debounce: a new status is only reported after it holds for N consecutive samples; until then the last reported status is held. Symmetric (escalation and recovery both debounced), keyed per check so memory/load/per-mount disk/per-device smart debounce independently. State persists across loop iterations (pod lifetime). - FLAP_SAMPLES defaults to 1 (no debounce = unchanged behavior); raise to 3 to ride out transient spikes. Chart bumped to 0.2.27. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0813182 commit 43b24b1

4 files changed

Lines changed: 107 additions & 8 deletions

File tree

charts/tinymon-operator/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ apiVersion: v2
22
name: tinymon-operator
33
description: Kubernetes operator that syncs annotated resources to TinyMon
44
type: application
5-
version: 0.2.26
6-
appVersion: "0.2.26"
5+
version: 0.2.27
6+
appVersion: "0.2.27"

charts/tinymon-operator/templates/daemonset.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ spec:
4646
fieldPath: spec.nodeName
4747
- name: INTERVAL
4848
value: {{ .Values.nodeMonitor.interval | quote }}
49+
- name: FLAP_SAMPLES
50+
value: {{ .Values.nodeMonitor.flapSamples | quote }}
51+
{{- with .Values.nodeMonitor.thresholds }}
52+
- name: MEM_WARN_PCT
53+
value: {{ .memWarnPct | quote }}
54+
- name: MEM_CRIT_PCT
55+
value: {{ .memCritPct | quote }}
56+
- name: DISK_WARN_PCT
57+
value: {{ .diskWarnPct | quote }}
58+
- name: DISK_CRIT_PCT
59+
value: {{ .diskCritPct | quote }}
60+
- name: LOAD_WARN_PCT
61+
value: {{ .loadWarnPct | quote }}
62+
- name: LOAD_CRIT_PCT
63+
value: {{ .loadCritPct | quote }}
64+
{{- end }}
4965
{{- if .Values.nodeMonitor.diskIncludeMounts }}
5066
- name: DISK_INCLUDE_MOUNTS
5167
value: {{ .Values.nodeMonitor.diskIncludeMounts | quote }}

charts/tinymon-operator/values.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ nodeMonitor:
3434
diskIncludeMounts: ""
3535
# Comma-separated mount prefixes to exclude (empty = none)
3636
diskExcludeMounts: ""
37+
# Flap suppression: require this many consecutive samples at a new status
38+
# before reporting the change (1 = report every sample, no debounce).
39+
# Raise to 3 to ride out transient spikes (e.g. a backup job briefly
40+
# pushing memory over the line) without flapping the check.
41+
flapSamples: 1
42+
# Alert thresholds in percent (warning at *WarnPct, critical at *CritPct).
43+
thresholds:
44+
memWarnPct: 80
45+
memCritPct: 90
46+
diskWarnPct: 80
47+
diskCritPct: 90
48+
loadWarnPct: 80
49+
loadCritPct: 90
3750
resources:
3851
requests:
3952
cpu: 10m

scripts/node-monitor.sh

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,75 @@ set -eu
77
: "${NODE_NAME:?NODE_NAME is required}"
88
: "${INTERVAL:=60}"
99

10+
# Alert thresholds (percent). warning at *_WARN_PCT, critical at *_CRIT_PCT.
11+
: "${MEM_WARN_PCT:=80}"
12+
: "${MEM_CRIT_PCT:=90}"
13+
: "${DISK_WARN_PCT:=80}"
14+
: "${DISK_CRIT_PCT:=90}"
15+
: "${LOAD_WARN_PCT:=80}"
16+
: "${LOAD_CRIT_PCT:=90}"
17+
18+
# Flap suppression: require this many consecutive samples at a new status
19+
# before reporting the change. 1 = report every sample immediately (no
20+
# debounce). Higher values smooth out transient spikes (e.g. a backup job
21+
# briefly pushing memory over the line) that would otherwise flap the check.
22+
: "${FLAP_SAMPLES:=1}"
23+
1024
HOST_ADDRESS="k8s://${CLUSTER_NAME}/node/${NODE_NAME}"
1125

26+
# Per-check debounce state survives across loop iterations (pod-lifetime).
27+
STATE_DIR="${STATE_DIR:-/tmp/tinymon-node-monitor-state}"
28+
mkdir -p "$STATE_DIR"
29+
1230
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $*" >&2; }
1331

32+
# smooth_status <key> <raw_status>
33+
# Debounces status transitions: a new status is only reported after it has
34+
# held for FLAP_SAMPLES consecutive samples. Until then the last reported
35+
# status is held. State is keyed by <key> so each check (memory, load,
36+
# per-mount disk, per-device disk_health) debounces independently.
37+
smooth_status() {
38+
key="$1"
39+
raw="$2"
40+
41+
# No debounce requested -> pass through unchanged.
42+
if [ "$FLAP_SAMPLES" -le 1 ]; then
43+
echo "$raw"
44+
return
45+
fi
46+
47+
f="${STATE_DIR}/$(echo "$key" | tr '/:.' '___')"
48+
if [ -f "$f" ]; then
49+
IFS='|' read -r last pending count < "$f"
50+
else
51+
last="$raw"; pending="$raw"; count=0
52+
fi
53+
: "${last:=$raw}"; : "${pending:=$raw}"; : "${count:=0}"
54+
55+
if [ "$raw" = "$last" ]; then
56+
# Stable at the reported status -> reset any pending change.
57+
printf '%s|%s|%s\n' "$last" "$last" 0 > "$f"
58+
echo "$last"
59+
return
60+
fi
61+
62+
if [ "$raw" = "$pending" ]; then
63+
count=$((count + 1))
64+
else
65+
pending="$raw"; count=1
66+
fi
67+
68+
if [ "$count" -ge "$FLAP_SAMPLES" ]; then
69+
# Change has held long enough -> commit it.
70+
printf '%s|%s|%s\n' "$raw" "$raw" 0 > "$f"
71+
echo "$raw"
72+
else
73+
# Hold the last reported status while the change is still pending.
74+
printf '%s|%s|%s\n' "$last" "$pending" "$count" > "$f"
75+
echo "$last"
76+
fi
77+
}
78+
1479
# Format bytes to human-readable (Gi / Mi)
1580
fmt_bytes() {
1681
local bytes=$1
@@ -77,9 +142,10 @@ collect_disk() {
77142
local used_h=$(fmt_bytes $used_bytes)
78143

79144
local status="ok"
80-
if [ "$pct_raw" -ge 90 ]; then status="critical"
81-
elif [ "$pct_raw" -ge 80 ]; then status="warning"
145+
if [ "$pct_raw" -ge "$DISK_CRIT_PCT" ]; then status="critical"
146+
elif [ "$pct_raw" -ge "$DISK_WARN_PCT" ]; then status="warning"
82147
fi
148+
status=$(smooth_status "disk:${host_mount}" "$status")
83149

84150
local display_mount="$host_mount"
85151
local config=$(jq -cn --arg m "$display_mount" '{mount: $m}')
@@ -122,9 +188,10 @@ collect_memory() {
122188
local used_h=$(fmt_bytes $used_bytes)
123189

124190
local status="ok"
125-
if [ "$pct_int" -ge 90 ]; then status="critical"
126-
elif [ "$pct_int" -ge 80 ]; then status="warning"
191+
if [ "$pct_int" -ge "$MEM_CRIT_PCT" ]; then status="critical"
192+
elif [ "$pct_int" -ge "$MEM_WARN_PCT" ]; then status="warning"
127193
fi
194+
status=$(smooth_status "memory" "$status")
128195

129196
jq -cn \
130197
--arg ha "$HOST_ADDRESS" \
@@ -152,9 +219,10 @@ collect_load() {
152219
local pct=$(awk "BEGIN { printf \"%.0f\", $load1 / $ncpu * 100 }")
153220

154221
local status="ok"
155-
if [ "$pct" -ge 90 ]; then status="critical"
156-
elif [ "$pct" -ge 80 ]; then status="warning"
222+
if [ "$pct" -ge "$LOAD_CRIT_PCT" ]; then status="critical"
223+
elif [ "$pct" -ge "$LOAD_WARN_PCT" ]; then status="warning"
157224
fi
225+
status=$(smooth_status "load" "$status")
158226

159227
jq -cn \
160228
--arg ha "$HOST_ADDRESS" \
@@ -204,6 +272,8 @@ collect_disk_health() {
204272
msg="${msg}, ${temp}°C"
205273
fi
206274

275+
status=$(smooth_status "disk_health:${devname}" "$status")
276+
207277
echo $(jq -cn \
208278
--arg ha "$HOST_ADDRESS" \
209279
--arg st "$status" \

0 commit comments

Comments
 (0)