master
c 859 lines 37.9 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "health_internals.h"
4
5 #define DYNCFG_HEALTH_ALERT_PROTOTYPE_PREFIX "health:alert:prototype"
6
7 static void health_dyncfg_register_prototype(RRD_ALERT_PROTOTYPE *ap);
8
9 static char *health_dyncfg_alert_prototype_id_strdupz(const char *alert_name) {
10 size_t prefix_len = strlen(DYNCFG_HEALTH_ALERT_PROTOTYPE_PREFIX);
11 size_t alert_name_len = strlen(alert_name);
12 size_t id_len = prefix_len + 1 + alert_name_len;
13 char *id = mallocz(id_len + 1);
14
15 int written = snprintfz(id, id_len + 1, DYNCFG_HEALTH_ALERT_PROTOTYPE_PREFIX ":%s", alert_name);
16 internal_fatal((size_t)written != id_len,
17 "HEALTH DYNCFG: failed to build dyncfg id for alert '%s'", alert_name);
18
19 return id;
20 }
21
22 // ---------------------------------------------------------------------------------------------------------------------
23 // parse the json object of an alert definition
24
25 static void dims_grouping_to_rrdr_options(RRD_ALERT_PROTOTYPE *ap) {
26 ap->config.options &= ~(RRDR_OPTIONS_DIMS_AGGREGATION);
27
28 switch(ap->config.dims_group) {
29 default:
30 case ALERT_LOOKUP_DIMS_SUM:
31 break;
32
33 case ALERT_LOOKUP_DIMS_AVERAGE:
34 ap->config.options |= RRDR_OPTION_DIMS_AVERAGE;
35 break;
36
37 case ALERT_LOOKUP_DIMS_MIN:
38 ap->config.options |= RRDR_OPTION_DIMS_MIN;
39 break;
40
41 case ALERT_LOOKUP_DIMS_MAX:
42 ap->config.options |= RRDR_OPTION_DIMS_MAX;
43 break;
44
45 case ALERT_LOOKUP_DIMS_MIN2MAX:
46 ap->config.options |= RRDR_OPTION_DIMS_MIN2MAX;
47 break;
48 }
49 }
50
51 static void data_source_to_rrdr_options(RRD_ALERT_PROTOTYPE *ap) {
52 ap->config.options &= ~(RRDR_OPTIONS_DATA_SOURCES);
53
54 switch(ap->config.data_source) {
55 default:
56 case ALERT_LOOKUP_DATA_SOURCE_SAMPLES:
57 break;
58
59 case ALERT_LOOKUP_DATA_SOURCE_PERCENTAGES:
60 ap->config.options |= RRDR_OPTION_PERCENTAGE;
61 break;
62
63 case ALERT_LOOKUP_DATA_SOURCE_ANOMALIES:
64 ap->config.options |= RRDR_OPTION_ANOMALY_BIT;
65 break;
66 }
67 }
68
69 static bool parse_match(json_object *jobj, const char *path, struct rrd_alert_match *match, BUFFER *error, unsigned flags) {
70 STRING *on = NULL;
71 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "on", on, error, flags);
72 if(match->is_template)
73 match->on.context = on;
74 else
75 match->on.chart = on;
76
77 JSONC_PARSE_TXT2PATTERN_OR_ERROR_AND_RETURN(jobj, path, "host_labels", match->host_labels, error, flags);
78 JSONC_PARSE_TXT2PATTERN_OR_ERROR_AND_RETURN(jobj, path, "instance_labels", match->chart_labels, error, flags);
79
80 return true;
81 }
82
83 static bool parse_config_value_database_lookup(json_object *jobj, const char *path, struct rrd_alert_config *config, BUFFER *error, unsigned flags) {
84 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "after", config->after, error, flags);
85 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "before", config->before, error, flags);
86 JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, path, "time_group", time_grouping_txt2id, config->time_group, error, flags);
87 JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, path, "dims_group", alerts_dims_grouping2id, config->dims_group, error, flags);
88 JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, path, "data_source", alerts_data_sources2id, config->data_source, error, flags);
89
90 switch(config->time_group) {
91 default:
92 break;
93
94 case RRDR_GROUPING_COUNTIF:
95 JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, path, "time_group_condition", alerts_group_condition2id, config->time_group_condition, error, flags);
96 // fall through
97
98 case RRDR_GROUPING_TRIMMED_MEAN:
99 case RRDR_GROUPING_TRIMMED_MEDIAN:
100 case RRDR_GROUPING_PERCENTILE:
101 JSONC_PARSE_DOUBLE_OR_ERROR_AND_RETURN(jobj, path, "time_group_value", config->time_group_value, error, flags);
102 break;
103 }
104
105 JSONC_PARSE_ARRAY_OF_TXT2BITMAP_OR_ERROR_AND_RETURN(jobj, path, "options", rrdr_options_parse_one, config->options, error, flags);
106 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "dimensions", config->dimensions, error, flags);
107 return true;
108 }
109
110 static bool parse_config_value(json_object *jobj, const char *path, struct rrd_alert_config *config, BUFFER *error, unsigned flags) {
111 JSONC_PARSE_SUBOBJECT_CB(jobj, path, "database_lookup", config, parse_config_value_database_lookup, error, flags);
112 JSONC_PARSE_TXT2EXPRESSION_OR_ERROR_AND_RETURN(jobj, path, "calculation", config->calculation, error, JSONC_OPTIONAL);
113 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "units", config->units, error, JSONC_OPTIONAL);
114 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "update_every", config->update_every, error, flags);
115 return true;
116 }
117
118 static bool parse_config_conditions(json_object *jobj, const char *path, struct rrd_alert_config *config, BUFFER *error, unsigned flags) {
119 JSONC_PARSE_TXT2EXPRESSION_OR_ERROR_AND_RETURN(jobj, path, "warning_condition", config->warning, error, flags);
120 JSONC_PARSE_TXT2EXPRESSION_OR_ERROR_AND_RETURN(jobj, path, "critical_condition", config->critical, error, flags);
121 return true;
122 }
123
124 static bool parse_config_action_delay(json_object *jobj, const char *path, struct rrd_alert_config *config, BUFFER *error, unsigned flags) {
125 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "up", config->delay_up_duration, error, flags);
126 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "down", config->delay_down_duration, error, flags);
127 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "max", config->delay_max_duration, error, flags);
128 JSONC_PARSE_DOUBLE_OR_ERROR_AND_RETURN(jobj, path, "multiplier", config->delay_multiplier, error, flags);
129 return true;
130 }
131
132 static bool parse_config_action_repeat(json_object *jobj, const char *path, struct rrd_alert_config *config, BUFFER *error, unsigned flags) {
133 JSONC_PARSE_BOOL_OR_ERROR_AND_RETURN(jobj, path, "enabled", config->has_custom_repeat_config, error, flags);
134 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "warning", config->warn_repeat_every, error, flags);
135 JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, path, "critical", config->crit_repeat_every, error, flags);
136 return true;
137 }
138
139 static bool parse_config_action(json_object *jobj, const char *path, struct rrd_alert_config *config, BUFFER *error, unsigned flags) {
140 JSONC_PARSE_ARRAY_OF_TXT2BITMAP_OR_ERROR_AND_RETURN(jobj, path, "options", alert_action_options_parse_one, config->alert_action_options, error, flags);
141 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "execute", config->exec, error, flags);
142 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "recipient", config->recipient, error, flags);
143 JSONC_PARSE_SUBOBJECT_CB(jobj, path, "delay", config, parse_config_action_delay, error, flags);
144 JSONC_PARSE_SUBOBJECT_CB(jobj, path, "repeat", config, parse_config_action_repeat, error, flags);
145 return true;
146 }
147
148 static bool parse_config(json_object *jobj, const char *path, RRD_ALERT_PROTOTYPE *ap, BUFFER *error, unsigned flags) {
149 // we shouldn't parse these from the payload - they are given to us via the function call
150 // JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, path, "source_type", dyncfg_source_type2id, ap->config.source_type, error, flags);
151 // JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "source", ap->config.source, error, flags);
152
153 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "summary", ap->config.summary, error, JSONC_OPTIONAL);
154 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "info", ap->config.info, error, JSONC_OPTIONAL);
155 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "type", ap->config.type, error, JSONC_OPTIONAL);
156 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "component", ap->config.component, error, JSONC_OPTIONAL);
157 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "classification", ap->config.classification, error, JSONC_OPTIONAL);
158
159 JSONC_PARSE_SUBOBJECT_CB(jobj, path, "value", &ap->config, parse_config_value, error, flags);
160 JSONC_PARSE_SUBOBJECT_CB(jobj, path, "conditions", &ap->config, parse_config_conditions, error, JSONC_OPTIONAL);
161 JSONC_PARSE_SUBOBJECT_CB(jobj, path, "action", &ap->config, parse_config_action, error, JSONC_OPTIONAL);
162 JSONC_PARSE_SUBOBJECT_CB(jobj, path, "match", &ap->match, parse_match, error, flags);
163
164 return true;
165 }
166
167 static bool parse_prototype(json_object *jobj, const char *path, RRD_ALERT_PROTOTYPE *base, BUFFER *error, const char *name, unsigned flags) {
168 int64_t version = 0;
169 JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, path, "format_version", version, error, flags);
170
171 if(version != 1) {
172 buffer_sprintf(error, "unsupported document version");
173 return false;
174 }
175
176 JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "name", base->config.name, error, JSONC_REQUIRE_IF((!name || !*name) && (flags & JSONC_REQUIRED)));
177
178 json_object *rules;
179 if (json_object_object_get_ex(jobj, "rules", &rules)) {
180 if (json_object_get_type(rules) != json_type_array) {
181 buffer_sprintf(error, "member 'rules' is not an array");
182 return false;
183 }
184
185 size_t rules_len = json_object_array_length(rules);
186
187 RRD_ALERT_PROTOTYPE *ap = base; // fill the first entry
188 for (size_t i = 0; i < rules_len; i++) {
189 if(!ap) {
190 ap = callocz(1, sizeof(*base));
191 ap->config.name = string_dup(base->config.name);
192 DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(base->_internal.next, ap, _internal.prev, _internal.next);
193 }
194
195 json_object *rule = json_object_array_get_idx(rules, i);
196
197 JSONC_PARSE_BOOL_OR_ERROR_AND_RETURN(rule, path, "enabled", ap->match.enabled, error, flags);
198
199 char type[32];
200 JSONC_PARSE_TXT2CHAR_OR_ERROR_AND_RETURN(rule, path, "type", type, error, flags);
201 if(strcmp(type, "template") == 0)
202 ap->match.is_template = true;
203 else if(strcmp(type, "instance") == 0)
204 ap->match.is_template = false;
205 else {
206 buffer_sprintf(error, "type is '%s', but it can only be 'instance' or 'template'", type);
207 return false;
208 }
209
210 JSONC_PARSE_SUBOBJECT_CB(rule, path, "config", ap, parse_config, error, flags);
211
212 ap = NULL; // so that we will create another one, if available
213 }
214 }
215 else {
216 buffer_sprintf(error, "the rules array is missing");
217 return false;
218 }
219
220 return true;
221 }
222
223 static RRD_ALERT_PROTOTYPE *health_prototype_payload_parse(const char *payload, size_t payload_len, BUFFER *error, const char *name, unsigned flags) {
224 RRD_ALERT_PROTOTYPE *base = callocz(1, sizeof(*base));
225 CLEAN_JSON_OBJECT *jobj = NULL;
226
227 struct json_tokener *tokener = json_tokener_new();
228 if (!tokener) {
229 buffer_sprintf(error, "failed to allocate memory for json tokener");
230 goto cleanup;
231 }
232
233 jobj = json_tokener_parse_ex(tokener, payload, (int)payload_len);
234 if (json_tokener_get_error(tokener) != json_tokener_success) {
235 const char *error_msg = json_tokener_error_desc(json_tokener_get_error(tokener));
236 buffer_sprintf(error, "failed to parse json payload: %s", error_msg);
237 json_tokener_free(tokener);
238 goto cleanup;
239 }
240 json_tokener_free(tokener);
241
242 if(!parse_prototype(jobj, "", base, error, name, flags))
243 goto cleanup;
244
245 if(!base->config.name && name)
246 base->config.name = string_strdupz(name);
247
248 if(name && *name && string_strcmp(base->config.name, name) != 0) {
249 string_freez(base->config.name);
250 base->config.name = string_strdupz(name);
251 }
252
253 int i = 1;
254 for(RRD_ALERT_PROTOTYPE *ap = base; ap; ap = ap->_internal.next, i++) {
255 if(ap->config.name != base->config.name) {
256 string_freez(ap->config.name);
257 ap->config.name = string_dup(base->config.name);
258 }
259
260 if(!RRDCALC_HAS_DB_LOOKUP(ap) && !ap->config.calculation && (flags & (JSONC_REQUIRED | JSONC_STRICT))) {
261 buffer_sprintf(error, "Item %d has neither database lookup nor calculation", i - 1);
262 goto cleanup;
263 }
264
265 data_source_to_rrdr_options(ap);
266 dims_grouping_to_rrdr_options(ap);
267
268 if(ap->match.enabled)
269 base->_internal.enabled = true;
270 }
271
272 return base;
273
274 cleanup:
275 health_prototype_free(base);
276 return NULL;
277 }
278
279 // ---------------------------------------------------------------------------------------------------------------------
280 // generate the json object of an alert definition
281
282 static inline void health_prototype_rule_to_json_array_member(BUFFER *wb, RRD_ALERT_PROTOTYPE *ap, bool for_hashing) {
283 buffer_json_add_array_item_object(wb);
284 {
285 buffer_json_member_add_boolean(wb, "enabled", ap->match.enabled);
286 buffer_json_member_add_string(wb, "type", ap->match.is_template ? "template" : "instance");
287
288 buffer_json_member_add_object(wb, "config");
289 {
290 if(!for_hashing) {
291 buffer_json_member_add_uuid(wb, "hash", ap->config.hash_id);
292 buffer_json_member_add_string(wb, "source_type", dyncfg_id2source_type(ap->config.source_type));
293 buffer_json_member_add_string(wb, "source", string2str(ap->config.source));
294 }
295
296 buffer_json_member_add_object(wb, "match");
297 {
298 if(ap->match.is_template)
299 buffer_json_member_add_string(wb, "on", string2str(ap->match.on.context));
300 else
301 buffer_json_member_add_string(wb, "on", string2str(ap->match.on.chart));
302
303 buffer_json_member_add_string_or_empty(wb, "host_labels", ap->match.host_labels ? string2str(ap->match.host_labels) : "*");
304 buffer_json_member_add_string_or_empty(wb, "instance_labels", ap->match.chart_labels ? string2str(ap->match.chart_labels) : "*");
305 }
306 buffer_json_object_close(wb); // match
307
308 buffer_json_member_add_string(wb, "summary", string2str(ap->config.summary));
309 buffer_json_member_add_string(wb, "info", string2str(ap->config.info));
310
311 buffer_json_member_add_string(wb, "type", string2str(ap->config.type));
312 buffer_json_member_add_string(wb, "component", string2str(ap->config.component));
313 buffer_json_member_add_string(wb, "classification", string2str(ap->config.classification));
314
315 buffer_json_member_add_object(wb, "value");
316 {
317 buffer_json_member_add_object(wb, "database_lookup");
318 {
319 buffer_json_member_add_int64(wb, "after", ap->config.after);
320 buffer_json_member_add_int64(wb, "before", ap->config.before);
321 buffer_json_member_add_string(wb, "time_group", time_grouping_id2txt(ap->config.time_group));
322 buffer_json_member_add_string(wb, "time_group_condition", alerts_group_conditions_id2txt(ap->config.time_group_condition));
323 buffer_json_member_add_double(wb, "time_group_value", ap->config.time_group_value);
324 buffer_json_member_add_string(wb, "dims_group", alerts_dims_grouping_id2group(ap->config.dims_group));
325 buffer_json_member_add_string(wb, "data_source", alerts_data_source_id2source(ap->config.data_source));
326 rrdr_options_to_buffer_json_array(wb, "options", RRDR_OPTIONS_REMOVE_OVERLAPPING(ap->config.options));
327 buffer_json_member_add_string(wb, "dimensions", string2str(ap->config.dimensions));
328 }
329 buffer_json_object_close(wb); // database lookup
330
331 buffer_json_member_add_string(wb, "calculation", expression_source(ap->config.calculation));
332 buffer_json_member_add_string(wb, "units", string2str(ap->config.units));
333 buffer_json_member_add_uint64(wb, "update_every", ap->config.update_every);
334 }
335 buffer_json_object_close(wb); // value
336
337 buffer_json_member_add_object(wb, "conditions");
338 {
339 buffer_json_member_add_string(wb, "warning_condition", expression_source(ap->config.warning));
340 buffer_json_member_add_string(wb, "critical_condition", expression_source(ap->config.critical));
341 }
342 buffer_json_object_close(wb); // conditions
343
344 buffer_json_member_add_object(wb, "action");
345 {
346 alert_action_options_to_buffer_json_array(wb, "options", ap->config.alert_action_options);
347 buffer_json_member_add_string(wb, "execute", string2str(ap->config.exec));
348 buffer_json_member_add_string(wb, "recipient", string2str(ap->config.recipient));
349
350 buffer_json_member_add_object(wb, "delay");
351 {
352 buffer_json_member_add_int64(wb, "up", ap->config.delay_up_duration);
353 buffer_json_member_add_int64(wb, "down", ap->config.delay_down_duration);
354 buffer_json_member_add_int64(wb, "max", ap->config.delay_max_duration);
355 buffer_json_member_add_double(wb, "multiplier", ap->config.delay_multiplier);
356 }
357 buffer_json_object_close(wb); // delay
358
359 buffer_json_member_add_object(wb, "repeat");
360 {
361 buffer_json_member_add_boolean(wb, "enabled", ap->config.has_custom_repeat_config);
362 buffer_json_member_add_uint64(wb, "warning", ap->config.has_custom_repeat_config ? ap->config.warn_repeat_every : 0);
363 buffer_json_member_add_uint64(wb, "critical", ap->config.has_custom_repeat_config ? ap->config.crit_repeat_every : 0);
364 }
365 buffer_json_object_close(wb); // repeat
366 }
367 buffer_json_object_close(wb); // action
368 }
369 buffer_json_object_close(wb); // match
370 }
371 buffer_json_object_close(wb); // array item
372 }
373
374 void health_prototype_to_json(BUFFER *wb, RRD_ALERT_PROTOTYPE *ap, bool for_hashing) {
375 buffer_flush(wb);
376 buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
377
378 buffer_json_member_add_uint64(wb, "format_version", 1);
379 buffer_json_member_add_string(wb, "name", string2str(ap->config.name));
380 buffer_json_member_add_array(wb, "rules");
381 {
382 for(RRD_ALERT_PROTOTYPE *t = ap; t ; t = t->_internal.next)
383 health_prototype_rule_to_json_array_member(wb, t, for_hashing);
384 }
385 buffer_json_array_close(wb); // rules
386 buffer_json_finalize(wb);
387 }
388
389 // ---------------------------------------------------------------------------------------------------------------------
390
391 static inline void dyncfg_user_config_print_duration(BUFFER *wb, const char *prefix, int seconds) {
392 if((seconds % 3600) == 0)
393 buffer_sprintf(wb, "%s%dh", prefix?prefix:"", seconds / 3600);
394 else if((seconds % 60) == 0)
395 buffer_sprintf(wb, "%s%dm", prefix?prefix:"", seconds / 60);
396 else
397 buffer_sprintf(wb, "%s%ds", prefix?prefix:"", seconds);
398 }
399
400 int dyncfg_health_prototype_to_conf(BUFFER *wb, RRD_ALERT_PROTOTYPE *ap, const char *name) {
401 buffer_flush(wb);
402 wb->content_type = CT_TEXT_PLAIN;
403 wb->expires = now_realtime_sec();
404
405 int n = 0;
406 for(RRD_ALERT_PROTOTYPE *nap = ap; nap ; nap = nap->_internal.next) {
407 if(++n > 1)
408 buffer_sprintf(wb, "\n");
409
410 if(nap->match.is_template) {
411 buffer_sprintf(wb, "%13s: %s\n", "template", name);
412 buffer_sprintf(wb, "%13s: %s\n", "on", string2str(nap->match.on.context));
413 }
414 else {
415 buffer_sprintf(wb, "%13s: %s\n", "alarm", name);
416 buffer_sprintf(wb, "%13s: %s\n", "on", string2str(nap->match.on.chart));
417 }
418
419 if(nap->config.classification)
420 buffer_sprintf(wb, "%13s: %s\n", "class", string2str(nap->config.classification));
421
422 if(nap->config.type)
423 buffer_sprintf(wb, "%13s: %s\n", "type", string2str(nap->config.type));
424
425 if(nap->config.component)
426 buffer_sprintf(wb, "%13s: %s\n", "component", string2str(nap->config.component));
427
428 if(nap->match.host_labels)
429 buffer_sprintf(wb, "%13s: %s\n", "host labels", string2str(nap->match.host_labels));
430
431 if(nap->match.chart_labels)
432 buffer_sprintf(wb, "%13s: %s\n", "chart labels", string2str(nap->match.chart_labels));
433
434 if(nap->config.after) {
435 buffer_sprintf(wb, "%13s: %s", "lookup", time_grouping_tostring(nap->config.time_group));
436 switch(nap->config.time_group) {
437 case RRDR_GROUPING_PERCENTILE:
438 case RRDR_GROUPING_TRIMMED_MEAN:
439 case RRDR_GROUPING_TRIMMED_MEDIAN:
440 buffer_sprintf(wb, "(%0.2f)", nap->config.time_group_value);
441 break;
442
443 case RRDR_GROUPING_COUNTIF:
444 buffer_sprintf(wb, "(%s%0.2f)", alerts_group_conditions_id2txt(nap->config.time_group_condition), nap->config.time_group_value);
445 break;
446
447 default:
448 break;
449 }
450
451 dyncfg_user_config_print_duration(wb, " ", nap->config.after);
452
453 if(nap->config.before)
454 dyncfg_user_config_print_duration(wb, " at ", nap->config.before);
455
456 if(nap->config.options) {
457 buffer_strcat(wb, " ");
458 rrdr_options_to_buffer(wb, nap->config.options);
459 }
460
461 if(nap->config.dimensions)
462 buffer_sprintf(wb, " of %s", string2str(nap->config.dimensions));
463
464 buffer_strcat(wb, "\n");
465 }
466
467 if(nap->config.calculation)
468 buffer_sprintf(wb, "%13s: %s\n", "calc", expression_source(nap->config.calculation));
469
470 if(nap->config.units)
471 buffer_sprintf(wb, "%13s: %s\n", "units", string2str(nap->config.units));
472
473 if(nap->config.update_every) {
474 buffer_sprintf(wb, "%13s: ", "every");
475 dyncfg_user_config_print_duration(wb, NULL, nap->config.update_every);
476 buffer_strcat(wb, "\n");
477 }
478
479 if(nap->config.warning)
480 buffer_sprintf(wb, "%13s: %s\n", "warn", expression_source(nap->config.warning));
481
482 if(nap->config.critical)
483 buffer_sprintf(wb, "%13s: %s\n", "crit", expression_source(nap->config.critical));
484
485 if(nap->config.delay_up_duration || nap->config.delay_down_duration) {
486 buffer_sprintf(wb, "%13s:", "delay");
487
488 if(nap->config.delay_up_duration)
489 dyncfg_user_config_print_duration(wb, " up ", nap->config.delay_up_duration);
490
491 if(nap->config.delay_down_duration)
492 dyncfg_user_config_print_duration(wb, " down ", nap->config.delay_down_duration);
493
494 if(nap->config.delay_multiplier)
495 buffer_sprintf(wb, " multiplier %0.2f", nap->config.delay_multiplier);
496
497 if(nap->config.delay_max_duration)
498 dyncfg_user_config_print_duration(wb, " max ", nap->config.delay_max_duration);
499
500 buffer_strcat(wb, "\n");
501 }
502
503 if(nap->config.alert_action_options) {
504 buffer_sprintf(wb, "%13s:", "options");
505 alert_action_options_to_buffer(wb, nap->config.alert_action_options);
506 buffer_strcat(wb, "\n");
507 }
508
509 if (nap->config.has_custom_repeat_config) {
510 buffer_sprintf(wb, "%13s:", "repeat");
511
512 if (!nap->config.crit_repeat_every && !nap->config.warn_repeat_every) {
513 buffer_strcat(wb, " off\n");
514 } else {
515 dyncfg_user_config_print_duration(wb, " warning ", (int)nap->config.warn_repeat_every);
516 dyncfg_user_config_print_duration(wb, " critical ", (int)nap->config.crit_repeat_every);
517 buffer_strcat(wb, "\n");
518 }
519 }
520
521 if(nap->config.summary)
522 buffer_sprintf(wb, "%13s: %s\n", "summary", string2str(nap->config.summary));
523
524 if(nap->config.info)
525 buffer_sprintf(wb, "%13s: %s\n", "info", string2str(nap->config.info));
526
527 if(nap->config.exec && nap->config.exec != localhost->health.default_exec)
528 buffer_sprintf(wb, "%13s: %s\n", "exec", string2str(nap->config.exec));
529
530 if(nap->config.recipient)
531 buffer_sprintf(wb, "%13s: %s\n", "to", string2str(nap->config.recipient));
532 }
533
534 return 200;
535 }
536
537 // ---------------------------------------------------------------------------------------------------------------------
538
539 static size_t dyncfg_health_remove_all_rrdcalc_of_prototype(STRING *alert_name) {
540 size_t removed = 0;
541
542 RRDHOST *host;
543 dfe_start_reentrant(rrdhost_root_index, host) {
544 if(!host->health.enabled || !rrdhost_flag_check(host, RRDHOST_FLAG_INITIALIZED_HEALTH))
545 continue;
546
547 RRDCALC *rc;
548 foreach_rrdcalc_in_rrdhost_reentrant(host, rc) {
549 if(rc->config.name != alert_name)
550 continue;
551
552 rrdcalc_unlink_and_delete(host, rc, false);
553 removed++;
554 }
555 foreach_rrdcalc_in_rrdhost_done(rc);
556 dictionary_garbage_collect(host->rrdcalc_root_index);
557 }
558 dfe_done(host);
559
560 return removed;
561 }
562
563 static void dyncfg_health_prototype_reapply(RRD_ALERT_PROTOTYPE *ap) {
564 health_prototype_apply_to_all_hosts(ap);
565 }
566
567 static int dyncfg_health_prototype_template_action(BUFFER *result, DYNCFG_CMDS cmd, const char *add_name, BUFFER *payload, const char *source __maybe_unused) {
568 int code = HTTP_RESP_INTERNAL_SERVER_ERROR;
569 switch(cmd) {
570 case DYNCFG_CMD_ADD: {
571 CLEAN_BUFFER *error = buffer_create(0, NULL);
572 RRD_ALERT_PROTOTYPE *nap = health_prototype_payload_parse(buffer_tostring(payload), buffer_strlen(payload), error, add_name, JSONC_REQUIRED);
573 if(!nap)
574 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, buffer_tostring(error));
575 else {
576 char *msg = "";
577
578 nap->config.source_type = DYNCFG_SOURCE_TYPE_DYNCFG;
579 bool added = health_prototype_add(nap, &msg); // this swaps ap <-> nap
580
581 if(!added) {
582 health_prototype_free(nap);
583 if(!msg || !*msg) msg = "required attributes are missing";
584 return dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, msg);
585 }
586 else
587 freez(nap);
588
589 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(health_globals.prototypes.dict, add_name);
590 if(!item)
591 return dyncfg_default_response(result, HTTP_RESP_INTERNAL_SERVER_ERROR, "added prototype is not found");
592
593 RRD_ALERT_PROTOTYPE *ap = dictionary_acquired_item_value(item);
594
595 dyncfg_health_prototype_reapply(ap);
596 health_dyncfg_register_prototype(ap);
597 code = ap->_internal.enabled ? DYNCFG_RESP_ACCEPTED : DYNCFG_RESP_ACCEPTED_DISABLED;
598 dictionary_acquired_item_release(health_globals.prototypes.dict, item);
599
600 code = dyncfg_default_response(result, code, "accepted");
601 }
602 }
603 break;
604
605 case DYNCFG_CMD_USERCONFIG: {
606 CLEAN_BUFFER *error = buffer_create(0, NULL);
607 RRD_ALERT_PROTOTYPE *nap = health_prototype_payload_parse(buffer_tostring(payload), buffer_strlen(payload), error, add_name, JSONC_OPTIONAL);
608 if(!nap)
609 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, buffer_tostring(error));
610 else {
611 code = dyncfg_health_prototype_to_conf(result, nap, add_name);
612 health_prototype_free(nap);
613 }
614 }
615 break;
616
617 case DYNCFG_CMD_SCHEMA:
618 code = dyncfg_default_response(result, HTTP_RESP_NOT_IMPLEMENTED, "schema not implemented yet for prototype templates");
619 break;
620
621 case DYNCFG_CMD_TEST:
622 code = dyncfg_default_response(result, HTTP_RESP_NOT_IMPLEMENTED, "test not implemented yet for prototype templates");
623 break;
624
625 case DYNCFG_CMD_REMOVE:
626 case DYNCFG_CMD_RESTART:
627 case DYNCFG_CMD_DISABLE:
628 case DYNCFG_CMD_ENABLE:
629 case DYNCFG_CMD_UPDATE:
630 case DYNCFG_CMD_GET:
631 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "action given is not supported for prototype templates");
632 break;
633
634 case DYNCFG_CMD_NONE:
635 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "invalid action received for prototype templates");
636 break;
637 }
638
639 return code;
640 }
641
642 static int dyncfg_health_prototype_job_action(BUFFER *result, DYNCFG_CMDS cmd, BUFFER *payload, const char *source __maybe_unused, const char *alert_name) {
643 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(health_globals.prototypes.dict, alert_name);
644 if(!item)
645 return dyncfg_default_response(result, HTTP_RESP_NOT_FOUND, "no alert prototype is available by the name given");
646
647 RRD_ALERT_PROTOTYPE *ap = dictionary_acquired_item_value(item);
648 CLEAN_CHAR_P *alert_name_dyncfg = health_dyncfg_alert_prototype_id_strdupz(alert_name);
649
650 int code = HTTP_RESP_INTERNAL_SERVER_ERROR;
651
652 switch(cmd) {
653 case DYNCFG_CMD_SCHEMA:
654 code = dyncfg_default_response(result, HTTP_RESP_NOT_IMPLEMENTED, "schema not implemented yet");
655 break;
656
657 case DYNCFG_CMD_GET:
658 health_prototype_to_json(result, ap, false);
659 code = HTTP_RESP_OK;
660 break;
661
662 case DYNCFG_CMD_DISABLE:
663 if(ap->_internal.enabled) {
664 ap->_internal.enabled = false;
665 dyncfg_health_prototype_reapply(ap);
666 dyncfg_status(localhost, alert_name_dyncfg, DYNCFG_STATUS_DISABLED);
667 code = dyncfg_default_response(result, HTTP_RESP_OK, "disabled");
668 }
669 else
670 code = dyncfg_default_response(result, HTTP_RESP_OK, "already disabled");
671 break;
672
673 case DYNCFG_CMD_ENABLE:
674 if(ap->_internal.enabled)
675 code = dyncfg_default_response(result, HTTP_RESP_OK, "already enabled");
676 else {
677 size_t matches_enabled = 0;
678 rw_spinlock_write_lock(&ap->_internal.rw_spinlock);
679 for(RRD_ALERT_PROTOTYPE *t = ap; t ;t = t->_internal.next)
680 if(t->match.enabled)
681 matches_enabled++;
682 rw_spinlock_write_unlock(&ap->_internal.rw_spinlock);
683
684 if(!matches_enabled) {
685 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "all rules in this alert are disabled, so enabling the alert has no effect");
686 }
687 else {
688 ap->_internal.enabled = true;
689 dyncfg_health_prototype_reapply(ap);
690 dyncfg_status(localhost, alert_name_dyncfg, DYNCFG_STATUS_ACCEPTED);
691 code = dyncfg_default_response(result, DYNCFG_RESP_ACCEPTED, "enabled");
692 }
693 }
694 break;
695
696 case DYNCFG_CMD_UPDATE: {
697 CLEAN_BUFFER *error = buffer_create(0, NULL);
698 RRD_ALERT_PROTOTYPE *nap = health_prototype_payload_parse(buffer_tostring(payload), buffer_strlen(payload), error, alert_name, JSONC_REQUIRED);
699 if(!nap)
700 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, buffer_tostring(error));
701 else {
702 char *msg = "";
703 nap->config.source_type = DYNCFG_SOURCE_TYPE_DYNCFG;
704 bool added = health_prototype_add(nap, &msg); // this swaps ap <-> nap
705
706 if(!added) {
707 health_prototype_free(nap);
708 if(!msg || !*msg) msg = "required attributes are missing";
709 return dyncfg_default_response( result, HTTP_RESP_BAD_REQUEST, msg);
710 }
711 else
712 freez(nap);
713
714 dyncfg_health_prototype_reapply(ap);
715 code = ap->_internal.enabled ? DYNCFG_RESP_ACCEPTED : DYNCFG_RESP_ACCEPTED_DISABLED;
716 code = dyncfg_default_response(result, code, "updated");
717 }
718 }
719 break;
720
721 case DYNCFG_CMD_USERCONFIG: {
722 CLEAN_BUFFER *error = buffer_create(0, NULL);
723 RRD_ALERT_PROTOTYPE *nap = health_prototype_payload_parse(buffer_tostring(payload), buffer_strlen(payload), error, alert_name, JSONC_OPTIONAL);
724 if(!nap)
725 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, buffer_tostring(error));
726 else {
727 code = dyncfg_health_prototype_to_conf(result, nap, alert_name);
728 health_prototype_free(nap);
729 }
730 }
731 break;
732
733 case DYNCFG_CMD_REMOVE:
734 dyncfg_health_remove_all_rrdcalc_of_prototype(ap->config.name);
735 dictionary_del(health_globals.prototypes.dict, dictionary_acquired_item_name(item));
736 code = dyncfg_default_response(result, HTTP_RESP_OK, "deleted");
737 dyncfg_del(localhost, alert_name_dyncfg);
738 break;
739
740 case DYNCFG_CMD_TEST:
741 case DYNCFG_CMD_ADD:
742 case DYNCFG_CMD_RESTART:
743 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "action given is not supported for the prototype job");
744 break;
745
746 case DYNCFG_CMD_NONE:
747 code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "invalid action received");
748 break;
749 }
750
751 dictionary_acquired_item_release(health_globals.prototypes.dict, item);
752 return code;
753 }
754
755 int dyncfg_health_cb(const char *transaction __maybe_unused, const char *id, DYNCFG_CMDS cmd, const char *add_name,
756 BUFFER *payload, usec_t *stop_monotonic_ut __maybe_unused, bool *cancelled __maybe_unused,
757 BUFFER *result, HTTP_ACCESS access __maybe_unused, const char *source, void *data __maybe_unused) {
758 CLEAN_CHAR_P *buf = strdupz(id);
759
760 char *words[100] = { NULL };
761 size_t num_words = quoted_strings_splitter_dyncfg_id(buf, words, 100);
762 size_t i = 0;
763 int code = HTTP_RESP_INTERNAL_SERVER_ERROR;
764
765 char *health_prefix = get_word(words, num_words, i++);
766 if(!health_prefix || !*health_prefix || strcmp(health_prefix, "health") != 0)
767 return dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "first component of id is not 'health'");
768
769 char *alert_prefix = get_word(words, num_words, i++);
770 if(!alert_prefix || !*alert_prefix || strcmp(alert_prefix, "alert") != 0)
771 return dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "second component of id is not 'alert'");
772
773 char *type_prefix = get_word(words, num_words, i++);
774 if(!type_prefix || !*type_prefix || strcmp(type_prefix, "prototype") != 0)
775 return dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, "third component of id is not 'prototype'");
776
777 char *alert_name = get_word(words, num_words, i++);
778 if(!alert_name || !*alert_name) {
779 // action on the prototype template
780
781 code = dyncfg_health_prototype_template_action(result, cmd, add_name, payload, source);
782 }
783 else {
784 // action on a specific alert prototype
785
786 code = dyncfg_health_prototype_job_action(result, cmd, payload, source, alert_name);
787 }
788 return code;
789 }
790
791 void health_dyncfg_unregister_all_prototypes(void) {
792 RRD_ALERT_PROTOTYPE *ap;
793
794 // remove dyncfg
795 // it is ok if they are not added before
796
797 dfe_start_read(health_globals.prototypes.dict, ap) {
798 CLEAN_CHAR_P *key = health_dyncfg_alert_prototype_id_strdupz(string2str(ap->config.name));
799 dyncfg_del(localhost, key);
800 }
801 dfe_done(ap);
802 dyncfg_del(localhost, DYNCFG_HEALTH_ALERT_PROTOTYPE_PREFIX);
803 }
804
805 static void health_dyncfg_register_prototype(RRD_ALERT_PROTOTYPE *ap) {
806 CLEAN_CHAR_P *key = health_dyncfg_alert_prototype_id_strdupz(string2str(ap->config.name));
807
808 // bool trace = false;
809 // if(string_strcmp(ap->config.name, "ram_available") == 0)
810 // trace = true;
811
812 dyncfg_add(localhost, key, "/health/alerts/prototypes",
813 ap->_internal.enabled ? DYNCFG_STATUS_ACCEPTED : DYNCFG_STATUS_DISABLED, DYNCFG_TYPE_JOB,
814 ap->config.source_type, string2str(ap->config.source),
815 DYNCFG_CMD_SCHEMA | DYNCFG_CMD_GET | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE |
816 DYNCFG_CMD_UPDATE | DYNCFG_CMD_USERCONFIG |
817 (ap->config.source_type == DYNCFG_SOURCE_TYPE_DYNCFG /* && !ap->_internal.is_on_disk */ ? DYNCFG_CMD_REMOVE : 0),
818 HTTP_ACCESS_NONE,
819 HTTP_ACCESS_NONE,
820 dyncfg_health_cb, NULL);
821
822 #ifdef NETDATA_TEST_HEALTH_PROTOTYPES_JSON_AND_PARSING
823 {
824 // make sure we can generate valid json, parse it back and come up to the same object
825
826 CLEAN_BUFFER *original = buffer_create(0, NULL);
827 CLEAN_BUFFER *parsed = buffer_create(0, NULL);
828 CLEAN_BUFFER *error = buffer_create(0, NULL);
829 health_prototype_to_json(original, ap, true);
830 RRD_ALERT_PROTOTYPE *t = health_prototype_payload_parse(buffer_tostring(original), buffer_strlen(original), error, string2str(ap->config.name), JSONC_REQUIRED);
831 if(!t)
832 fatal("hey! cannot parse: %s", buffer_tostring(error));
833
834 health_prototype_to_json(parsed, t, true);
835
836 if(strcmp(buffer_tostring(original), buffer_tostring(parsed)) != 0)
837 fatal("hey! they are different!");
838 }
839 #endif
840 }
841
842 void health_dyncfg_register_all_prototypes(void) {
843 RRD_ALERT_PROTOTYPE *ap;
844
845 dyncfg_add(localhost,
846 DYNCFG_HEALTH_ALERT_PROTOTYPE_PREFIX, "/health/alerts/prototypes",
847 DYNCFG_STATUS_ACCEPTED, DYNCFG_TYPE_TEMPLATE,
848 DYNCFG_SOURCE_TYPE_INTERNAL, "internal",
849 DYNCFG_CMD_SCHEMA | DYNCFG_CMD_ADD | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE | DYNCFG_CMD_USERCONFIG,
850 HTTP_ACCESS_NONE,
851 HTTP_ACCESS_NONE,
852 dyncfg_health_cb, NULL);
853
854 dfe_start_read(health_globals.prototypes.dict, ap) {
855 if(ap->config.source_type != DYNCFG_SOURCE_TYPE_DYNCFG)
856 health_dyncfg_register_prototype(ap);
857 }
858 dfe_done(ap);
859 }