@cryptotaxi247 / netdata-1 / commits / 60c905e4d

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;