Fix health alert db lookup parser (#21529)
Costa Tsaousis committed
Jan 12, 2026 at 05:43 UTC
60c905e4de80752ec62d8875d955cad03a98d358
9 files changed
+434
-20
CMakeLists.txt
+2
@@ -1414,6 +1414,8 @@ set(HEALTH_PLUGIN_FILES
1414
src/health/health.c
1415
src/health/health.h
1416
src/health/health_config.c
1417
+ src/health/health-config-unittest.c
1418
+ src/health/health-config-unittest.h
1419
src/health/health_json.c
1420
src/health/health_log.c
1421
src/health/health_prototypes.c
src/daemon/main.c
+6
@@ -219,6 +219,7 @@ int progress_unittest(void);
219
int dyncfg_unittest(void);
220
int eval_unittest(void);
221
int duration_unittest(void);
222
+int health_config_unittest(void);
223
bool netdata_random_session_id_generate(void);
224
225
#ifdef OS_WINDOWS
@@ -407,6 +408,7 @@ int netdata_main(int argc, char **argv) {
408
if (dyncfg_unittest()) return 1;
409
if (eval_unittest()) return 1;
410
if (duration_unittest()) return 1;
411
+ if (health_config_unittest()) return 1;
412
if (unittest_waiting_queue()) return 1;
413
if (uuidmap_unittest()) return 1;
414
#ifdef HAVE_LIBBACKTRACE
@@ -527,6 +529,10 @@ int netdata_main(int argc, char **argv) {
529
unittest_running = true;
530
return duration_unittest();
531
}
532
+ else if(strcmp(optarg, "healthconfigtest") == 0) {
533
+ unittest_running = true;
534
+ return health_config_unittest();
535
+ }
536
else if(strcmp(optarg, "dyncfgtest") == 0) {
537
unittest_running = true;
538
if(unittest_prepare_rrd(&user))
src/health/REFERENCE.md
+1
-1
@@ -527,7 +527,7 @@ lookup: METHOD(GROUPING OPTIONS) AFTER [at BEFORE] [every DURATION] [OPTIONS] [o
527
528
| Parameter | Purpose | Details |
529
|--------------------|-----------------------------|----------------------------------------------------------------------|
530
-| `GROUPING OPTIONS` | Conditional processing | `CONDITION VALUE` where condition is `!=`, `=`, `<=`, `<`, `>`, `>=` |
530
+| `GROUPING OPTIONS` | Conditional processing | `CONDITION VALUE` where condition is `!=`, `=`, `==`, `<=`, `<`, `>`, `>=` |
531
| `at BEFORE` | End of lookup timeframe | Default is 0 (now) |
532
| `every DURATION` | Update frequency | Supports `s`, `m`, `h`, `d` units |
533
| `OPTIONS` | Processing modifiers | See options table below |
src/health/health-config-unittest.c
new
+365
@@ -0,0 +1,365 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+#include "health_internals.h"
4
+#include "health-config-unittest.h"
5
+#include "web/api/queries/query.h"
6
+
7
+// test case structure for db lookup parsing
8
+typedef struct {
9
+ const char *input; // lookup string to parse
10
+ bool should_succeed; // expected parsing result
11
+ RRDR_TIME_GROUPING expected_group; // expected grouping method
12
+ ALERT_LOOKUP_TIME_GROUP_CONDITION expected_cond; // expected condition (for countif)
13
+ NETDATA_DOUBLE expected_value; // expected value (for countif/percentile)
14
+ int32_t expected_after; // expected after duration in seconds
15
+ int32_t expected_before; // expected before (0 or offset)
16
+ const char *description; // test description
17
+} db_lookup_test_case_t;
18
+
19
+// mark value as "don't care" for tests that don't use it
20
+#define DC_VALUE NAN
21
+#define DC_COND ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL
22
+
23
+static const db_lookup_test_case_t test_cases[] = {
24
+ // =========================================================================
25
+ // STOCK CONFIG PATTERNS - These are all patterns from src/health/health.d/*.conf
26
+ // =========================================================================
27
+
28
+ // Basic grouping methods with duration
29
+ { "average -10m", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -600, 0, "basic average" },
30
+ { "sum -1m", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -60, 0, "basic sum" },
31
+ { "max -10m", true, RRDR_GROUPING_MAX, DC_COND, DC_VALUE, -600, 0, "basic max" },
32
+ { "min -5m", true, RRDR_GROUPING_MIN, DC_COND, DC_VALUE, -300, 0, "basic min" },
33
+ { "avg -1m", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, 0, "avg alias" },
34
+
35
+ // Duration variations
36
+ { "average -5s", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -5, 0, "seconds duration" },
37
+ { "average -1h", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -3600, 0, "hour duration" },
38
+ { "average -30s", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -30, 0, "30 seconds" },
39
+ { "average -2h", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -7200, 0, "2 hours" },
40
+ { "average -20m", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -1200, 0, "20 minutes" },
41
+
42
+ // With 'at' offset (before parameter)
43
+ { "max -2h at -15m", true, RRDR_GROUPING_MAX, DC_COND, DC_VALUE, -7200, -900, "with at offset" },
44
+ { "min -10m at -50m", true, RRDR_GROUPING_MIN, DC_COND, DC_VALUE, -600, -3000, "min with offset" },
45
+ { "average -1m at -10s", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, -10, "avg with small offset" },
46
+ { "max -2m at -1m", true, RRDR_GROUPING_MAX, DC_COND, DC_VALUE, -120, -60, "max with offset" },
47
+ { "average -5m at -5m", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -300, -300, "avg with equal offset" },
48
+
49
+ // With 'unaligned' option
50
+ { "average -5s unaligned", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -5, 0, "with unaligned" },
51
+ { "sum -1m unaligned", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -60, 0, "sum unaligned" },
52
+ { "max -10s unaligned", true, RRDR_GROUPING_MAX, DC_COND, DC_VALUE, -10, 0, "max unaligned" },
53
+
54
+ // With 'absolute' option
55
+ { "average -1m unaligned absolute", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, 0, "with absolute" },
56
+ { "sum -10m unaligned absolute", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -600, 0, "sum absolute" },
57
+
58
+ // With 'percentage' option
59
+ { "average -1m unaligned percentage", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, 0, "with percentage" },
60
+
61
+ // With 'of' dimension filter
62
+ { "max -10m every 1m of read_errs", true, RRDR_GROUPING_MAX, DC_COND, DC_VALUE, -600, 0, "with of dimension" },
63
+ { "average -10m unaligned of yellow", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -600, 0, "of single dim" },
64
+ { "average -1m unaligned of anomaly_rate", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, 0, "of anomaly_rate" },
65
+ { "average -10m unaligned of user,system,softirq,irq,guest", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -600, 0, "of multiple dims" },
66
+ { "average -1m unaligned absolute of !success,*", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, 0, "of negated pattern" },
67
+ { "sum -1m unaligned of success", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -60, 0, "sum of dim" },
68
+
69
+ // With 'match-names' option
70
+ { "max -1s unaligned match-names of BT,NG", true, RRDR_GROUPING_MAX, DC_COND, DC_VALUE, -1, 0, "with match-names" },
71
+ { "average -10m unaligned match-names of used", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -600, 0, "avg match-names" },
72
+ { "average -60s unaligned absolute match-names of overwritten", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, 0, "absolute match-names" },
73
+
74
+ // With 'every' option
75
+ { "max -10m every 1m of read_errs", true, RRDR_GROUPING_MAX, DC_COND, DC_VALUE, -600, 0, "with every" },
76
+
77
+ // Complex combinations from stock configs
78
+ { "average -10m unaligned of iowait", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -600, 0, "cpu iowait" },
79
+ { "sum -30m unaligned", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -1800, 0, "ram 30m sum" },
80
+ { "sum -30m unaligned absolute of out", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -1800, 0, "swap out" },
81
+ { "sum -10m unaligned absolute of received", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -600, 0, "net received" },
82
+ { "average -60s unaligned absolute of ListenOverflows", true, RRDR_GROUPING_AVERAGE, DC_COND, DC_VALUE, -60, 0, "tcp listen" },
83
+ { "sum -1m unaligned absolute", true, RRDR_GROUPING_SUM, DC_COND, DC_VALUE, -60, 0, "bcache errors" },
84
+
85
+ // =========================================================================
86
+ // PARAMETERIZED AGGREGATION FUNCTIONS
87
+ // =========================================================================
88
+
89
+ // countif with comparison operators
90
+ { "countif(>0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 0.5, -600, 0, "countif greater" },
91
+ { "countif(>=0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL, 0.5, -600, 0, "countif greater equal" },
92
+ { "countif(<0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS, 0.5, -600, 0, "countif less" },
93
+ { "countif(<=0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS_EQUAL, 0.5, -600, 0, "countif less equal" },
94
+ { "countif(!=0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL, 0.5, -600, 0, "countif not equal" },
95
+ { "countif(<>0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL, 0.5, -600, 0, "countif not equal alt" },
96
+ { "countif(0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.5, -600, 0, "countif equal (default)" },
97
+ { "countif(=0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.5, -600, 0, "countif explicit equal" },
98
+ { "countif(:0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.5, -600, 0, "countif colon equal" },
99
+ { "countif(==0.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.5, -600, 0, "countif double equal" },
100
+ { "countif(!5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL, 5.0, -600, 0, "countif bang not equal" },
101
+
102
+ // countif with integer values
103
+ { "countif(>0) -5m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 0.0, -300, 0, "countif >0" },
104
+ { "countif(>1) -5m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 1.0, -300, 0, "countif >1" },
105
+ { "countif(>100) -5m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 100.0, -300, 0, "countif >100" },
106
+
107
+ // countif with decimal starting with dot
108
+ { "countif(>.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 0.5, -600, 0, "countif >.5" },
109
+ { "countif(<.25) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS, 0.25, -600, 0, "countif <.25" },
110
+ { "countif(=.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.5, -600, 0, "countif =.5" },
111
+ { "countif(>=.1) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL, 0.1, -600, 0, "countif >=.1" },
112
+ { "countif(>-.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, -0.5, -600, 0, "countif >-.5" },
113
+ { "countif(<-.25) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS, -0.25, -600, 0, "countif <-.25" },
114
+
115
+ // countif with negative numbers
116
+ { "countif(>-3) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, -3.0, -600, 0, "countif >-3" },
117
+ { "countif(>=-3) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL, -3.0, -600, 0, "countif >=-3" },
118
+ { "countif(<-1.5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS, -1.5, -600, 0, "countif <-1.5" },
119
+ { "countif(=-10) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, -10.0, -600, 0, "countif =-10" },
120
+
121
+ // countif with explicit positive sign
122
+ { "countif(>+5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 5.0, -600, 0, "countif >+5" },
123
+ { "countif(=+0) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.0, -600, 0, "countif =+0" },
124
+
125
+ // countif with scientific notation
126
+ { "countif(>1e-5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 1e-5, -600, 0, "countif scientific" },
127
+ { "countif(<1.5e3) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS, 1500.0, -600, 0, "countif sci positive exp" },
128
+ { "countif(>=1E-10) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL, 1e-10, -600, 0, "countif sci uppercase E" },
129
+
130
+ // countif with empty parentheses (defaults to =0)
131
+ { "countif() -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.0, -600, 0, "countif default" },
132
+
133
+ // countif with whitespace inside parentheses
134
+ { "countif( >5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 5.0, -600, 0, "countif space before op" },
135
+ { "countif(> 5) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 5.0, -600, 0, "countif space after op" },
136
+ { "countif( > 5 ) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 5.0, -600, 0, "countif spaces around" },
137
+ { "countif( >= 0.5 ) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL, 0.5, -600, 0, "countif multi spaces" },
138
+
139
+ // countif with options
140
+ { "countif(>2.00) -10m unaligned of *", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 2.0, -600, 0, "countif with opts" },
141
+ { "countif(>0) -1m unaligned absolute", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 0.0, -60, 0, "countif absolute" },
142
+
143
+ // percentile variations
144
+ { "percentile( 95 ) -10m", true, RRDR_GROUPING_PERCENTILE, DC_COND, 95.0, -600, 0, "percentile with spaces" },
145
+ { "percentile(95) -10m", true, RRDR_GROUPING_PERCENTILE, DC_COND, 95.0, -600, 0, "percentile 95" },
146
+ { "percentile(99) -5m", true, RRDR_GROUPING_PERCENTILE, DC_COND, 99.0, -300, 0, "percentile 99" },
147
+ { "percentile(50) -10m", true, RRDR_GROUPING_PERCENTILE, DC_COND, 50.0, -600, 0, "percentile 50 (median)" },
148
+ { "percentile(75) -1h", true, RRDR_GROUPING_PERCENTILE, DC_COND, 75.0, -3600, 0, "percentile 75" },
149
+ { "percentile(90) -10m unaligned", true, RRDR_GROUPING_PERCENTILE, DC_COND, 90.0, -600, 0, "percentile unaligned" },
150
+
151
+ // percentile short forms (predefined)
152
+ { "percentile25 -10m", true, RRDR_GROUPING_PERCENTILE25, DC_COND, DC_VALUE, -600, 0, "percentile25" },
153
+ { "percentile50 -10m", true, RRDR_GROUPING_PERCENTILE50, DC_COND, DC_VALUE, -600, 0, "percentile50" },
154
+ { "percentile75 -10m", true, RRDR_GROUPING_PERCENTILE75, DC_COND, DC_VALUE, -600, 0, "percentile75" },
155
+ { "percentile90 -10m", true, RRDR_GROUPING_PERCENTILE90, DC_COND, DC_VALUE, -600, 0, "percentile90" },
156
+ { "percentile95 -10m", true, RRDR_GROUPING_PERCENTILE, DC_COND, DC_VALUE, -600, 0, "percentile95" },
157
+ { "percentile97 -10m", true, RRDR_GROUPING_PERCENTILE97, DC_COND, DC_VALUE, -600, 0, "percentile97" },
158
+ { "percentile98 -10m", true, RRDR_GROUPING_PERCENTILE98, DC_COND, DC_VALUE, -600, 0, "percentile98" },
159
+ { "percentile99 -10m", true, RRDR_GROUPING_PERCENTILE99, DC_COND, DC_VALUE, -600, 0, "percentile99" },
160
+
161
+ // trimmed-mean variations
162
+ { "trimmed-mean(5) -10m", true, RRDR_GROUPING_TRIMMED_MEAN, DC_COND, 5.0, -600, 0, "trimmed-mean 5%" },
163
+ { "trimmed-mean(10) -10m", true, RRDR_GROUPING_TRIMMED_MEAN, DC_COND, 10.0, -600, 0, "trimmed-mean 10%" },
164
+ { "trimmed-mean(1.00) -10m", true, RRDR_GROUPING_TRIMMED_MEAN, DC_COND, 1.0, -600, 0, "trimmed-mean 1%" },
165
+
166
+ // trimmed-mean short forms (predefined)
167
+ { "trimmed-mean1 -10m", true, RRDR_GROUPING_TRIMMED_MEAN1, DC_COND, DC_VALUE, -600, 0, "trimmed-mean1" },
168
+ { "trimmed-mean2 -10m", true, RRDR_GROUPING_TRIMMED_MEAN2, DC_COND, DC_VALUE, -600, 0, "trimmed-mean2" },
169
+ { "trimmed-mean3 -10m", true, RRDR_GROUPING_TRIMMED_MEAN3, DC_COND, DC_VALUE, -600, 0, "trimmed-mean3" },
170
+ { "trimmed-mean5 -10m", true, RRDR_GROUPING_TRIMMED_MEAN, DC_COND, DC_VALUE, -600, 0, "trimmed-mean5" },
171
+ { "trimmed-mean10 -10m", true, RRDR_GROUPING_TRIMMED_MEAN10, DC_COND, DC_VALUE, -600, 0, "trimmed-mean10" },
172
+ { "trimmed-mean15 -10m", true, RRDR_GROUPING_TRIMMED_MEAN15, DC_COND, DC_VALUE, -600, 0, "trimmed-mean15" },
173
+ { "trimmed-mean20 -10m", true, RRDR_GROUPING_TRIMMED_MEAN20, DC_COND, DC_VALUE, -600, 0, "trimmed-mean20" },
174
+ { "trimmed-mean25 -10m", true, RRDR_GROUPING_TRIMMED_MEAN25, DC_COND, DC_VALUE, -600, 0, "trimmed-mean25" },
175
+
176
+ // trimmed-mean with value in parentheses followed by N
177
+ { "trimmed-mean5(1.00) -10m", true, RRDR_GROUPING_TRIMMED_MEAN, DC_COND, 1.0, -600, 0, "trimmed-mean5 with value" },
178
+
179
+ // trimmed-median variations
180
+ { "trimmed-median(5) -10m", true, RRDR_GROUPING_TRIMMED_MEDIAN, DC_COND, 5.0, -600, 0, "trimmed-median 5%" },
181
+ { "trimmed-median1 -10m", true, RRDR_GROUPING_TRIMMED_MEDIAN1, DC_COND, DC_VALUE, -600, 0, "trimmed-median1" },
182
+ { "trimmed-median5 -10m", true, RRDR_GROUPING_TRIMMED_MEDIAN, DC_COND, DC_VALUE, -600, 0, "trimmed-median5" },
183
+
184
+ // median
185
+ { "median -10m", true, RRDR_GROUPING_MEDIAN, DC_COND, DC_VALUE, -600, 0, "median" },
186
+ { "median -5m unaligned", true, RRDR_GROUPING_MEDIAN, DC_COND, DC_VALUE, -300, 0, "median unaligned" },
187
+
188
+ // stddev
189
+ { "stddev -10m", true, RRDR_GROUPING_STDDEV, DC_COND, DC_VALUE, -600, 0, "stddev" },
190
+ { "stddev -5m unaligned", true, RRDR_GROUPING_STDDEV, DC_COND, DC_VALUE, -300, 0, "stddev unaligned" },
191
+
192
+ // cv (coefficient of variation)
193
+ { "cv -10m", true, RRDR_GROUPING_CV, DC_COND, DC_VALUE, -600, 0, "cv" },
194
+
195
+ // ses (single exponential smoothing)
196
+ { "ses -10m", true, RRDR_GROUPING_SES, DC_COND, DC_VALUE, -600, 0, "ses" },
197
+ { "ema -10m", true, RRDR_GROUPING_SES, DC_COND, DC_VALUE, -600, 0, "ema alias" },
198
+
199
+ // des (double exponential smoothing)
200
+ { "des -10m", true, RRDR_GROUPING_DES, DC_COND, DC_VALUE, -600, 0, "des" },
201
+
202
+ // incremental-sum
203
+ { "incremental-sum -10m", true, RRDR_GROUPING_INCREMENTAL_SUM, DC_COND, DC_VALUE, -600, 0, "incremental-sum" },
204
+
205
+ // extremes
206
+ { "extremes -10m", true, RRDR_GROUPING_EXTREMES, DC_COND, DC_VALUE, -600, 0, "extremes" },
207
+
208
+ // =========================================================================
209
+ // ERROR CASES
210
+ // =========================================================================
211
+
212
+ // Missing duration
213
+ { "average", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "missing duration" },
214
+ { "sum", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "sum missing duration" },
215
+ { "percentile(95)", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "percentile missing duration" },
216
+ { "countif(>0.5)", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "countif missing duration" },
217
+
218
+ // Invalid grouping method
219
+ { "invalid -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "invalid method" },
220
+ { "foo -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "unknown method" },
221
+
222
+ // Invalid characters in group options
223
+ { "countif(>abc) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "invalid char in countif" },
224
+ { "percentile(abc) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "invalid char in percentile" },
225
+
226
+ // Malformed numeric values (lone dot, sign+dot without digits)
227
+ { "countif(.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "lone dot invalid" },
228
+ { "countif(+.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "plus dot invalid" },
229
+ { "countif(-.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "minus dot invalid" },
230
+ { "countif(>.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "greater dot invalid" },
231
+ { "countif(<.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "less dot invalid" },
232
+ { "countif(>=.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "greater-equal dot invalid" },
233
+ { "countif(<=.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "less-equal dot invalid" },
234
+ { "countif(>+.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "greater plus dot invalid" },
235
+ { "countif(<-.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "less minus dot invalid" },
236
+ { "percentile(.) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "percentile lone dot invalid" },
237
+
238
+ // Invalid operator combinations
239
+ { "countif(===5) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "triple equals invalid" },
240
+ { "countif(>==5) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "greater double equals invalid" },
241
+ { "countif(<==5) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "less double equals invalid" },
242
+ { "countif(>::5) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "colon after greater invalid" },
243
+ { "countif(>=:5) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "colon after greater-equal invalid" },
244
+ { "countif(<:5) -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "colon after less invalid" },
245
+
246
+ // Operators with no value (should default to 0)
247
+ { "countif(=) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.0, -600, 0, "equals no value" },
248
+ { "countif(==) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.0, -600, 0, "double equals no value" },
249
+ { "countif(:) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL, 0.0, -600, 0, "colon no value" },
250
+ { "countif(>) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER, 0.0, -600, 0, "greater no value" },
251
+ { "countif(<) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS, 0.0, -600, 0, "less no value" },
252
+ { "countif(!) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL, 0.0, -600, 0, "bang no value" },
253
+ { "countif(!=) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL, 0.0, -600, 0, "not-equal no value" },
254
+ { "countif(<>) -10m", true, RRDR_GROUPING_COUNTIF, ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL, 0.0, -600, 0, "less-greater no value" },
255
+
256
+ // Missing closing parenthesis
257
+ { "countif(>0.5 -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "missing close paren" },
258
+ { "percentile(95 -10m", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "percentile missing paren" },
259
+
260
+ // Invalid duration
261
+ { "average -xyz", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "invalid duration" },
262
+ { "average abc", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "non-numeric duration" },
263
+
264
+ // Empty input
265
+ { "", false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, "empty input" },
266
+
267
+ // Sentinel
268
+ { NULL, false, RRDR_GROUPING_UNDEFINED, DC_COND, DC_VALUE, 0, 0, NULL }
269
+};
270
+
271
+static int run_db_lookup_test(const db_lookup_test_case_t *test) {
272
+ // create a copy of the input since parsing modifies the string
273
+ char buffer[1024];
274
+ strncpyz(buffer, test->input, sizeof(buffer) - 1);
275
+
276
+ struct rrd_alert_config ac = { 0 };
277
+ ac.time_group_value = NAN;
278
+
279
+ int result = health_parse_db_lookup(1, "unittest", buffer, &ac);
280
+ bool succeeded = (result != 0);
281
+
282
+ // check if success/failure matches expectation
283
+ if(succeeded != test->should_succeed) {
284
+ fprintf(stderr, "FAILED [%s]: expected %s but got %s\n",
285
+ test->description,
286
+ test->should_succeed ? "success" : "failure",
287
+ succeeded ? "success" : "failure");
288
+ return 1;
289
+ }
290
+
291
+ // if test should fail, we're done
292
+ if(!test->should_succeed)
293
+ return 0;
294
+
295
+ int errors = 0;
296
+
297
+ // verify grouping method
298
+ if(ac.time_group != test->expected_group) {
299
+ fprintf(stderr, "FAILED [%s]: expected group %u but got %u\n",
300
+ test->description, (unsigned)test->expected_group, (unsigned)ac.time_group);
301
+ errors++;
302
+ }
303
+
304
+ // verify after duration
305
+ if(ac.after != test->expected_after) {
306
+ fprintf(stderr, "FAILED [%s]: expected after %d but got %d\n",
307
+ test->description, test->expected_after, ac.after);
308
+ errors++;
309
+ }
310
+
311
+ // verify before (offset) if specified
312
+ if(test->expected_before != 0 && ac.before != test->expected_before) {
313
+ fprintf(stderr, "FAILED [%s]: expected before %d but got %d\n",
314
+ test->description, test->expected_before, ac.before);
315
+ errors++;
316
+ }
317
+
318
+ // verify condition for countif
319
+ if(test->expected_cond != DC_COND && ac.time_group_condition != test->expected_cond) {
320
+ fprintf(stderr, "FAILED [%s]: expected condition %d but got %d\n",
321
+ test->description, test->expected_cond, ac.time_group_condition);
322
+ errors++;
323
+ }
324
+
325
+ // verify value for countif/percentile/trimmed-mean if specified
326
+ if(!isnan(test->expected_value)) {
327
+ NETDATA_DOUBLE actual_value = isnan(ac.time_group_value) ? 0.0 : ac.time_group_value;
328
+ if(fabsl(actual_value - test->expected_value) > 0.0001) {
329
+ fprintf(stderr, "FAILED [%s]: expected value %f but got %f\n",
330
+ test->description, test->expected_value, actual_value);
331
+ errors++;
332
+ }
333
+ }
334
+
335
+ // cleanup
336
+ string_freez(ac.dimensions);
337
+
338
+ return errors;
339
+}
340
+
341
+int health_config_unittest(void) {
342
+ int passed = 0;
343
+ int failed = 0;
344
+
345
+ // initialize time grouping before running tests
346
+ time_grouping_init();
347
+
348
+ fprintf(stderr, "\nStarting health config db lookup parser unit tests\n");
349
+ fprintf(stderr, "===================================================\n\n");
350
+
351
+ for(const db_lookup_test_case_t *test = test_cases; test->input != NULL; test++) {
352
+ int errors = run_db_lookup_test(test);
353
+ if(errors == 0) {
354
+ passed++;
355
+ }
356
+ else {
357
+ failed += errors;
358
+ }
359
+ }
360
+
361
+ fprintf(stderr, "\n===================================================\n");
362
+ fprintf(stderr, "Health config parser tests: %d passed, %d failed\n\n", passed, failed);
363
+
364
+ return failed;
365
+}
src/health/health-config-unittest.h
new
+8
@@ -0,0 +1,8 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+#ifndef NETDATA_HEALTH_CONFIG_UNITTEST_H
4
+#define NETDATA_HEALTH_CONFIG_UNITTEST_H
5
+
6
+int health_config_unittest(void);
7
+
8
+#endif // NETDATA_HEALTH_CONFIG_UNITTEST_H
src/health/health_config.c
+42
-17
@@ -155,7 +155,7 @@ static inline int health_parse_repeat(
155
return 1;
156
}
157
158
-static inline int health_parse_db_lookup(size_t line, const char *filename, char *string, struct rrd_alert_config *ac) {
158
+int health_parse_db_lookup(size_t line, const char *filename, char *string, struct rrd_alert_config *ac) {
159
if(ac->dimensions) string_freez(ac->dimensions);
160
ac->dimensions = NULL;
161
ac->after = 0;
@@ -190,6 +190,9 @@ static inline int health_parse_db_lookup(size_t line, const char *filename, char
190
}
191
192
if(group_options) {
193
+ // skip leading whitespace inside parentheses
194
+ while(*s && isspace((uint8_t)*s)) s++;
195
+
196
if(*s == '!') {
197
s++;
198
if(*s == '=') s++;
@@ -203,36 +206,55 @@ static inline int health_parse_db_lookup(size_t line, const char *filename, char
206
}
207
else if(*s == '=') {
208
s++;
206
- ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL;
209
+ ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS_EQUAL;
210
}
211
else
209
- ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER;
212
+ ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS;
213
}
214
else if(*s == '>') {
215
+ s++;
216
if(*s == '=') {
217
s++;
214
- ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS_EQUAL;
218
+ ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL;
219
}
220
else
217
- ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS;
221
+ ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER;
222
+ }
223
+ else if(*s == '=' || *s == ':') {
224
+ // explicit equal operator (=, == or :)
225
+ s++;
226
+ if(*s == '=') s++; // support == as well
227
+ ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL;
228
}
229
230
while(*s && isspace((uint8_t)*s)) s++;
231
222
- if(*s) {
223
- if(isdigit((uint8_t)*s) || *s == '.') {
224
- ac->time_group_value = str2ndd(s, &s);
225
- while(s && *s && isspace((uint8_t)*s)) s++;
226
-
227
- if(!s || *s != ')') {
228
- netdata_log_error("Health configuration at line %zu of file '%s': missing closing parenthesis after number in aggregation method on '%s'",
229
- line, filename, key);
230
- return 0;
231
- }
232
+ if(*s == ')') {
233
+ // empty options like countif() - allowed, will use defaults
234
+ }
235
+ else if((isdigit((uint8_t)*s)) ||
236
+ (*s == '.' && isdigit((uint8_t)s[1])) ||
237
+ ((*s == '-' || *s == '+') && s[1] &&
238
+ ((isdigit((uint8_t)s[1])) || (s[1] == '.' && isdigit((uint8_t)s[2]))))) {
239
+ // parse numeric value (including negative numbers like >=-3, or >+5, or >-.5)
240
+ ac->time_group_value = str2ndd(s, &s);
241
+ while(s && *s && isspace((uint8_t)*s)) s++;
242
+
243
+ if(!s || *s != ')') {
244
+ netdata_log_error("Health configuration at line %zu of file '%s': missing closing parenthesis after number in aggregation method on '%s'",
245
+ line, filename, key);
246
+ return 0;
247
}
248
}
234
- else if(*s != ')') {
235
- netdata_log_error("Health configuration at line %zu of file '%s': missing closing parenthesis after method on '%s'",
249
+ else if(*s) {
250
+ // invalid character - not a valid start of a number or ')'
251
+ netdata_log_error("Health configuration at line %zu of file '%s': invalid character '%c' in aggregation method options on '%s'",
252
+ line, filename, *s, key);
253
+ return 0;
254
+ }
255
+ else {
256
+ // end of string without closing parenthesis
257
+ netdata_log_error("Health configuration at line %zu of file '%s': missing closing parenthesis after aggregation method on '%s'",
258
line, filename, key);
259
return 0;
260
}
@@ -262,6 +284,7 @@ static inline int health_parse_db_lookup(size_t line, const char *filename, char
284
}
285
286
// then is the 'after' time
287
+ while(*s && isspace((uint8_t)*s)) s++; // skip whitespace after group method
288
key = s;
289
while(*s && !isspace((uint8_t)*s)) s++;
290
while(*s && isspace((uint8_t)*s)) *s++ = '\0';
@@ -290,6 +313,7 @@ static inline int health_parse_db_lookup(size_t line, const char *filename, char
313
if (!duration_parse_seconds(value, &ac->before)) {
314
netdata_log_error("Health configuration at line %zu of file '%s': invalid duration '%s' for '%s' keyword",
315
line, filename, value, key);
316
+ return 0;
317
}
318
}
319
else if(!strcasecmp(key, HEALTH_EVERY_KEY)) {
@@ -300,6 +324,7 @@ static inline int health_parse_db_lookup(size_t line, const char *filename, char
324
if (!duration_parse_seconds(value, &ac->update_every)) {
325
netdata_log_error("Health configuration at line %zu of file '%s': invalid duration '%s' for '%s' keyword",
326
line, filename, value, key);
327
+ return 0;
328
}
329
}
330
else if(!strcasecmp(key, "absolute") || !strcasecmp(key, "abs") || !strcasecmp(key, "absolute_sum")) {
src/health/health_internals.h
+3
@@ -98,6 +98,9 @@ struct health_plugin_globals {
98
extern struct health_plugin_globals health_globals;
99
100
int health_readfile(const char *filename, void *data, bool stock_config);
101
+
102
+// for unit testing
103
+int health_parse_db_lookup(size_t line, const char *filename, char *string, struct rrd_alert_config *ac);
104
void unlink_alarm_notify_in_progress(ALARM_ENTRY *ae);
105
void wait_for_all_notifications_to_finish_before_allowing_health_to_be_cleaned_up(void);
106
src/web/api/queries/countif/README.md
+1
-1
@@ -7,7 +7,7 @@ CountIf returns the percentage of points in the database that satisfy the condit
7
The following conditions are available:
8
9
- `!` or `!=` or `<>`, different than
10
-- `=` or `:`, equal to
10
+- `=` or `==` or `:`, equal to
11
- `>`, greater than
12
- `<`, less than
13
- `>=`, greater or equal to
src/web/api/queries/countif/countif.h
+6
-1
@@ -64,8 +64,13 @@ static inline void tg_countif_create(RRDR *r, const char *options __maybe_unused
64
}
65
break;
66
67
- default:
67
case '=':
68
+ if(options[1] == '=')
69
+ options++; // support ==, skip to second '='
70
+ g->comparison = TG_COUNTIF_EQUAL;
71
+ break;
72
+
73
+ default:
74
case ':':
75
g->comparison = TG_COUNTIF_EQUAL;
76
break;