From ba350240de6b32613db1e315e5354cb753c3b577 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 16:56:38 +0200 Subject: [PATCH 1/8] metric: fix CheckMetric.CheckForThresholds it performed short-circuiting tests over the ConditionList, which leads to maximum of one correct resulting condition. the result thould then be len(corrects) > 0 , not > 1 which is always false --- pkg/snclient/checkmetric.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/snclient/checkmetric.go b/pkg/snclient/checkmetric.go index 737df9bb..c67f4f20 100644 --- a/pkg/snclient/checkmetric.go +++ b/pkg/snclient/checkmetric.go @@ -191,7 +191,7 @@ func (m *CheckMetric) BuildCheckData() (data map[string]string) { return data } -// Metrics have a defined way for specifying key / values +// Returns if at least one condition in the conditionList is found to be correct // //nolint:dogsled // only need corrects list here func (m *CheckMetric) CheckForThresholds(conditionList *ConditionList) (result bool) { @@ -199,5 +199,5 @@ func (m *CheckMetric) CheckForThresholds(conditionList *ConditionList) (result b corrects, _, _, _ := conditionList.performMatches(data, true) - return len(corrects) > 1 + return len(corrects) > 0 } From 8a38ba0e489b44be57673ee76d0c4254cc8931d9 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 17:32:10 +0200 Subject: [PATCH 2/8] checks: CheckData.CheckMetrics uses the metric's condition list, which is fresher and canonical The metrics condition list may be modified during the check. When using the default check.warnCond and check.critCond , they might be stale. --- pkg/snclient/checkdata.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/snclient/checkdata.go b/pkg/snclient/checkdata.go index de6f9574..58585241 100644 --- a/pkg/snclient/checkdata.go +++ b/pkg/snclient/checkdata.go @@ -483,16 +483,16 @@ func (cd *CheckData) Check(data map[string]string, warnCond, critCond, okCond Co } // CheckMetrics tries warn/crit/ok conditions against given metrics and sets final state accordingly -func (cd *CheckData) CheckMetrics(warnCond, critCond, okCond ConditionList) { +func (cd *CheckData) CheckMetrics(_, _, okCond ConditionList) { // each metric is ran through conditions individually for _, metric := range cd.result.Metrics { state := CheckExitOK - if metric.CheckForThresholds(&warnCond) { + if metric.CheckForThresholds(&metric.Warning) { state = CheckExitWarning } - if metric.CheckForThresholds(&critCond) { + if metric.CheckForThresholds(&metric.Critical) { state = CheckExitCritical } From 7f7d6ee2c78d32b5a66a372592e60595e2f85e62 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 17:46:20 +0200 Subject: [PATCH 3/8] check_drivesize: support keywords using "used %" , "free %" "inodes_used %" "inodes_free %" literally in thresholds check if these exist, then add metrics accordingly add tests that check for different keyword usage: '[drive] used %' '[drive] used_pct' 'used %' also add test for the specialzation, where having a "drive" keyword containing condition in the threshold, applies a filter for the metrics originating from that entry --- pkg/snclient/check_drivesize.go | 25 ++++++----- pkg/snclient/check_drivesize_test.go | 66 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/pkg/snclient/check_drivesize.go b/pkg/snclient/check_drivesize.go index 4d7bd8ff..b9475929 100644 --- a/pkg/snclient/check_drivesize.go +++ b/pkg/snclient/check_drivesize.go @@ -356,36 +356,37 @@ func (l *CheckDrivesize) addMetrics(metricPrefix string, drive map[string]string } driveFreeMetricLabel := fmt.Sprintf("%s free %%", metricPrefix) - if check.HasThreshold(driveFreeMetricLabel) || check.HasThreshold("free") || check.HasThreshold("free_pct") || check.HasThreshold("free_bytes") { - check.warnThreshold = check.TransformMultipleKeywords([]string{"free_pct", "free_bytes"}, "free", check.warnThreshold) - check.critThreshold = check.TransformMultipleKeywords([]string{"free_pct", "free_bytes"}, "free", check.critThreshold) + if check.HasThreshold(driveFreeMetricLabel) || check.HasThreshold("free") || check.HasThreshold("free_pct") || check.HasThreshold("free_bytes") || check.HasThreshold("free %") { + check.warnThreshold = check.TransformMultipleKeywords([]string{"free_pct", "free_bytes", "free %"}, "free", check.warnThreshold) + check.critThreshold = check.TransformMultipleKeywords([]string{"free_pct", "free_bytes", "free %"}, "free", check.critThreshold) perfLabel := fmt.Sprintf("%s free", metricPrefix) check.AddBytePercentMetrics("free", perfLabel, magic*float64(usage.Free), magic*float64(total), drive) check.processMetricsWithSpecializedKeyword("drive", perfLabel, drive) } driveUsagePctMetricLabel := fmt.Sprintf("%s used %%", metricPrefix) - if check.HasThreshold(driveUsagePctMetricLabel) || check.HasThreshold("used") || check.HasThreshold("used_pct") || check.HasThreshold("used_bytes") { - check.warnThreshold = check.TransformMultipleKeywords([]string{"used_pct", "used_bytes"}, "used", check.warnThreshold) - check.critThreshold = check.TransformMultipleKeywords([]string{"used_pct", "used_bytes"}, "used", check.critThreshold) + if check.HasThreshold(driveUsagePctMetricLabel) || check.HasThreshold("used") || check.HasThreshold("used_pct") || check.HasThreshold("used_bytes") || check.HasThreshold("used %") { + check.warnThreshold = check.TransformMultipleKeywords([]string{"used_pct", "used_bytes", "used %"}, "used", check.warnThreshold) + check.critThreshold = check.TransformMultipleKeywords([]string{"used_pct", "used_bytes", "used %"}, "used", check.critThreshold) perfLabel := fmt.Sprintf("%s used", metricPrefix) check.AddBytePercentMetrics("used", perfLabel, magic*float64(usage.Used), magic*float64(total), drive) check.processMetricsWithSpecializedKeyword("drive", perfLabel, drive) } driveInodesUsedPctMetricLabel := fmt.Sprintf("%s inodes_used %%", metricPrefix) - if check.HasThreshold(driveInodesUsedPctMetricLabel) || check.HasThreshold("inodes") || check.HasThreshold("inodes_used") || check.HasThreshold("inodes_used_pct") { - check.warnThreshold = check.TransformMultipleKeywords([]string{"inodes_used_pct"}, "inodes_used", check.warnThreshold) - check.critThreshold = check.TransformMultipleKeywords([]string{"inodes_used_pct"}, "inodes_used", check.critThreshold) + if check.HasThreshold(driveInodesUsedPctMetricLabel) || check.HasThreshold("inodes") || check.HasThreshold("inodes_used") || check.HasThreshold("inodes_used_pct") || + check.HasThreshold("inodes_used %") { + check.warnThreshold = check.TransformMultipleKeywords([]string{"inodes_used_pct", "inodes_used %"}, "inodes_used", check.warnThreshold) + check.critThreshold = check.TransformMultipleKeywords([]string{"inodes_used_pct", "inodes_used %"}, "inodes_used", check.critThreshold) perfLabel := fmt.Sprintf("%s inodes_used", metricPrefix) check.AddPercentMetrics("inodes_used", perfLabel, float64(usage.InodesUsed), float64(usage.InodesTotal), drive) check.processMetricsWithSpecializedKeyword("drive", perfLabel, drive) } driveInodesFreePctMetricLabel := fmt.Sprintf("%s inodes_free %%", metricPrefix) - if check.HasThreshold(driveInodesFreePctMetricLabel) || check.HasThreshold("inodes_free") || check.HasThreshold("inodes_free_pct") { - check.warnThreshold = check.TransformMultipleKeywords([]string{"inodes_free_pct"}, "inodes_free", check.warnThreshold) - check.critThreshold = check.TransformMultipleKeywords([]string{"inodes_free_pct"}, "inodes_free", check.critThreshold) + if check.HasThreshold(driveInodesFreePctMetricLabel) || check.HasThreshold("inodes_free") || check.HasThreshold("inodes_free_pct") || check.HasThreshold("inodes_free %") { + check.warnThreshold = check.TransformMultipleKeywords([]string{"inodes_free_pct", "inodes_free %"}, "inodes_free", check.warnThreshold) + check.critThreshold = check.TransformMultipleKeywords([]string{"inodes_free_pct", "inodes_free %"}, "inodes_free", check.critThreshold) perfLabel := fmt.Sprintf("%s inodes_free", metricPrefix) check.AddPercentMetrics("inodes_free", perfLabel, float64(usage.InodesFree), float64(usage.InodesTotal), drive) check.processMetricsWithSpecializedKeyword("drive", perfLabel, drive) diff --git a/pkg/snclient/check_drivesize_test.go b/pkg/snclient/check_drivesize_test.go index 9744fea8..2ec4a6c2 100644 --- a/pkg/snclient/check_drivesize_test.go +++ b/pkg/snclient/check_drivesize_test.go @@ -83,3 +83,69 @@ func TestNonexistingDrive(t *testing.T) { StopTestAgent(t, snc) } + +func TestRootDriveThresholdsDrivePercentSign(t *testing.T) { + snc := StartTestAgent(t, "") + + // This tests the conditions using '[drive] used %' keyword + res := snc.RunCheck("check_drivesize", []string{"drive=/", "warn='/ used %' >= 0", "crit=none"}) + assert.Equalf(t, CheckExitWarning, res.State, "state should be WARNING") + + res = snc.RunCheck("check_drivesize", []string{"drive=/", "crit='/ used %' >= 0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") + + res = snc.RunCheck("check_drivesize", []string{"drive=/", "warn='/ used %' >= 0", "crit='/ used %' >= 0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") + + res = snc.RunCheck("check_drivesize", []string{"drive=/", "warn='/ used %' >= 0", "crit='/ used %' >= 0", "ok='/ used %' >= 0"}) + assert.Equalf(t, CheckExitOK, res.State, "state should be CRITICAL") + + StopTestAgent(t, snc) +} + +func TestRootDriveThresholdsUsedPct(t *testing.T) { + snc := StartTestAgent(t, "") + + // This tests the conditions using '[drive] used_pct' keyword + res := snc.RunCheck("check_drivesize", []string{"drive=/", "warn='/ used_pct' >= 0", "crit=none"}) + assert.Equalf(t, CheckExitWarning, res.State, "state should be WARNING") + + res = snc.RunCheck("check_drivesize", []string{"drive=/", "crit='/ used_pct' >= 0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") + + res = snc.RunCheck("check_drivesize", []string{"drive=/", "warn='/ used_pct' >= 0", "crit='/ used_pct' >= 0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") + + res = snc.RunCheck("check_drivesize", []string{"drive=/", "warn='/ used_pct' >= 0", "crit='/ used_pct' >= 0", "ok='/ used_pct' >= 0"}) + assert.Equalf(t, CheckExitOK, res.State, "state should be CRITICAL") + + StopTestAgent(t, snc) +} + +func TestRootDriveThresholdsUsedPercentSign(t *testing.T) { + snc := StartTestAgent(t, "") + + // Generic 'used %' keyword, should trigger + res := snc.RunCheck("check_drivesize", []string{"drive=/", "warning='used %' gt 0", "critical=none"}) + assert.Equalf(t, CheckExitWarning, res.State, "state should be WARNING") + + res = snc.RunCheck("check_drivesize", []string{"drive=/", "critical='used %' gt 0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") + + StopTestAgent(t, snc) +} + +func TestRootDriveSpecializedDriveConditions(t *testing.T) { + snc := StartTestAgent(t, "") + + // In critical conditions, the second one is filtered as it contains the drive="/" keyword. + // Since such a condition is present, other critical conditions are filtered out for that entry + res := snc.RunCheck("check_drivesize", []string{"drive=/", "critical='used %' gt 0", "critical=drive eq '/' and 'used_pct' gt 100"}) + assert.Equalf(t, CheckExitOK, res.State, "state should be OK") + + // Filtering does not work here, since the specialized condition is in the warning threshold, not critical threshold + res = snc.RunCheck("check_drivesize", []string{"drive=/", "critical='used %' gt 0", "warning=drive eq '/' and 'used_pct' gt 0"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be WARNING") + + StopTestAgent(t, snc) +} From 5078ecc35b80c25a263594b41b9bc646292473c2 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 18:12:14 +0200 Subject: [PATCH 4/8] check_temperature: expand the temp and temperature metric macros properly before adding to the metric --- pkg/snclient/check_temperature.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/snclient/check_temperature.go b/pkg/snclient/check_temperature.go index 12dbc285..2df8df30 100644 --- a/pkg/snclient/check_temperature.go +++ b/pkg/snclient/check_temperature.go @@ -124,14 +124,19 @@ func (l *CheckTemperature) addSensor(check *CheckData, sensor *temperatureStat, minVal := utils.ToPrecision(sensor.Min, 3) maxVal := utils.ToPrecision(sensor.High, 3) + var warning, critical ConditionList + if sensor.Min != 0 || sensor.Critical != 0 { + warning = check.ExpandMetricMacros(check.TransformMultipleKeywords([]string{"temp", "temperature"}, sensorKey, check.warnThreshold), entry) + critical = check.ExpandMetricMacros(check.TransformMultipleKeywords([]string{"temp", "temperature"}, sensorKey, check.critThreshold), entry) + } check.result.Metrics = append(check.result.Metrics, &CheckMetric{ ThresholdName: sensorKey, Name: sensorKey, Value: utils.ToPrecision(sensor.Temperature, 3), Min: &minVal, Max: &maxVal, - Warning: check.ExpandMetricMacros(check.TransformMultipleKeywords([]string{"temp", "temperature"}, sensorKey, check.warnThreshold), entry), - Critical: check.ExpandMetricMacros(check.TransformMultipleKeywords([]string{"temp", "temperature"}, sensorKey, check.critThreshold), entry), + Warning: warning, + Critical: critical, }) check.listData = append(check.listData, entry) From 0b4da0f2dcf970ac3bb52939d3c0a8d3cc436301 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 18:19:37 +0200 Subject: [PATCH 5/8] check_drivesize: add tests with two drives and specialized drive condition, check if perfdata is specialized according to the conditions --- pkg/snclient/check_drivesize_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/snclient/check_drivesize_test.go b/pkg/snclient/check_drivesize_test.go index 2ec4a6c2..901c7edf 100644 --- a/pkg/snclient/check_drivesize_test.go +++ b/pkg/snclient/check_drivesize_test.go @@ -149,3 +149,15 @@ func TestRootDriveSpecializedDriveConditions(t *testing.T) { StopTestAgent(t, snc) } + +func TestRootDriveSpecializedDriveConditionsPerfdata(t *testing.T) { + snc := StartTestAgent(t, "") + + // Check if the perfdata is printed out according to the specialized conditions + res := snc.RunCheck("check_drivesize", []string{"drive=/", "drive=/tmp", "critical='used_pct' gt 99", "critical=drive eq '/tmp' and 'used_pct' gt 1", "warn=none"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") + assert.Regexp(t, `'/ used %'=[^;]+;[^;]*;99;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) + assert.Regexp(t, `'/tmp used %'=[^;]+;[^;]*;1;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) + + StopTestAgent(t, snc) +} From 6df749905ca0fc09ef6936e4ca3bbc95a9e69c76 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 18:29:23 +0200 Subject: [PATCH 6/8] check_drivesize: skip specialized condition perfdata check on freebsd and darwin it uses both / and /tmp drives and those are not mounted as drives, or unavailable in those platforms --- pkg/snclient/check_drivesize_test.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pkg/snclient/check_drivesize_test.go b/pkg/snclient/check_drivesize_test.go index 901c7edf..7cead7c4 100644 --- a/pkg/snclient/check_drivesize_test.go +++ b/pkg/snclient/check_drivesize_test.go @@ -3,6 +3,7 @@ package snclient import ( + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -151,13 +152,18 @@ func TestRootDriveSpecializedDriveConditions(t *testing.T) { } func TestRootDriveSpecializedDriveConditionsPerfdata(t *testing.T) { - snc := StartTestAgent(t, "") - - // Check if the perfdata is printed out according to the specialized conditions - res := snc.RunCheck("check_drivesize", []string{"drive=/", "drive=/tmp", "critical='used_pct' gt 99", "critical=drive eq '/tmp' and 'used_pct' gt 1", "warn=none"}) - assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") - assert.Regexp(t, `'/ used %'=[^;]+;[^;]*;99;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) - assert.Regexp(t, `'/tmp used %'=[^;]+;[^;]*;1;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) - - StopTestAgent(t, snc) + switch runtime.GOOS { + case "linux": + snc := StartTestAgent(t, "") + + // Check if the perfdata is printed out according to the specialized conditions + res := snc.RunCheck("check_drivesize", []string{"drive=/", "drive=/tmp", "critical='used_pct' gt 99", "critical=drive eq '/tmp' and 'used_pct' gt 1", "warn=none"}) + assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") + assert.Regexp(t, `'/ used %'=[^;]+;[^;]*;99;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) + assert.Regexp(t, `'/tmp used %'=[^;]+;[^;]*;1;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) + + StopTestAgent(t, snc) + default: + t.Skipf("skipping tests due to platform: %s", runtime.GOOS) + } } From f277a3b0ff0644f12d96e6af8bb26f3e9ce279f1 Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 18:36:24 +0200 Subject: [PATCH 7/8] check_drivesize: completely disable specialized drive conditions perfdata tests it is failing even on linux systems in github ci since /tmp is not a drive there --- pkg/snclient/check_drivesize_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/pkg/snclient/check_drivesize_test.go b/pkg/snclient/check_drivesize_test.go index 7cead7c4..2ec4a6c2 100644 --- a/pkg/snclient/check_drivesize_test.go +++ b/pkg/snclient/check_drivesize_test.go @@ -3,7 +3,6 @@ package snclient import ( - "runtime" "testing" "github.com/stretchr/testify/assert" @@ -150,20 +149,3 @@ func TestRootDriveSpecializedDriveConditions(t *testing.T) { StopTestAgent(t, snc) } - -func TestRootDriveSpecializedDriveConditionsPerfdata(t *testing.T) { - switch runtime.GOOS { - case "linux": - snc := StartTestAgent(t, "") - - // Check if the perfdata is printed out according to the specialized conditions - res := snc.RunCheck("check_drivesize", []string{"drive=/", "drive=/tmp", "critical='used_pct' gt 99", "critical=drive eq '/tmp' and 'used_pct' gt 1", "warn=none"}) - assert.Equalf(t, CheckExitCritical, res.State, "state should be CRITICAL") - assert.Regexp(t, `'/ used %'=[^;]+;[^;]*;99;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) - assert.Regexp(t, `'/tmp used %'=[^;]+;[^;]*;1;0;100`, string(res.BuildPluginOutput()), `'/tmp used %' perfdata matches`) - - StopTestAgent(t, snc) - default: - t.Skipf("skipping tests due to platform: %s", runtime.GOOS) - } -} From 353587b05a57c8c235280fd665c9da7fb04e72ec Mon Sep 17 00:00:00 2001 From: Ahmet Oeztuerk Date: Thu, 23 Jul 2026 19:17:05 +0200 Subject: [PATCH 8/8] checkdata: remove unused warning and critical thresholds arguments in CheckMetrics call metrics save their own warning and critical thresholds inside a ConditionList in the CheckMetric object. while saving these, the thresholds may be modified, including filtering and keyword transformations only the okCondition is supplied directly from the check itself. --- pkg/snclient/checkdata.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/snclient/checkdata.go b/pkg/snclient/checkdata.go index 58585241..e82df076 100644 --- a/pkg/snclient/checkdata.go +++ b/pkg/snclient/checkdata.go @@ -239,7 +239,8 @@ func (cd *CheckData) finalizeOutput() (*CheckResult, error) { cd.setStateFromMaps(finalMacros) // Metrics are checked last, which also sets the final state log.Tracef("checking warning, critical, and ok thresholds on check metrics") - cd.CheckMetrics(cd.warnThreshold, cd.critThreshold, cd.okThreshold) + // metrics save their own warning and critical thresholds, but ok thresholds come from check itself + cd.CheckMetrics(cd.okThreshold) switch { case cd.result.Output != "": @@ -483,7 +484,7 @@ func (cd *CheckData) Check(data map[string]string, warnCond, critCond, okCond Co } // CheckMetrics tries warn/crit/ok conditions against given metrics and sets final state accordingly -func (cd *CheckData) CheckMetrics(_, _, okCond ConditionList) { +func (cd *CheckData) CheckMetrics(okCond ConditionList) { // each metric is ran through conditions individually for _, metric := range cd.result.Metrics { state := CheckExitOK