| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "health.h" |
| 4 | #include "health_internals.h" |
| 5 | |
| 6 | static inline int health_parse_delay( |
| 7 | size_t line, const char *filename, char *string, |
| 8 | int *delay_up_duration, |
| 9 | int *delay_down_duration, |
| 10 | int *delay_max_duration, |
| 11 | float *delay_multiplier) { |
| 12 | |
| 13 | char given_up = 0; |
| 14 | char given_down = 0; |
| 15 | char given_max = 0; |
| 16 | char given_multiplier = 0; |
| 17 | |
| 18 | char *s = string; |
| 19 | while(*s) { |
| 20 | char *key = s; |
| 21 | |
| 22 | while(*s && !isspace((uint8_t)*s)) s++; |
| 23 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 24 | |
| 25 | if(!*key) break; |
| 26 | |
| 27 | char *value = s; |
| 28 | while(*s && !isspace((uint8_t)*s)) s++; |
| 29 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 30 | |
| 31 | if(!strcasecmp(key, "up")) { |
| 32 | if (!duration_parse_seconds(value, delay_up_duration)) { |
| 33 | netdata_log_error("Health configuration at line %zu of file '%s': invalid value '%s' for '%s' keyword", |
| 34 | line, filename, value, key); |
| 35 | } |
| 36 | else given_up = 1; |
| 37 | } |
| 38 | else if(!strcasecmp(key, "down")) { |
| 39 | if (!duration_parse_seconds(value, delay_down_duration)) { |
| 40 | netdata_log_error("Health configuration at line %zu of file '%s': invalid value '%s' for '%s' keyword", |
| 41 | line, filename, value, key); |
| 42 | } |
| 43 | else given_down = 1; |
| 44 | } |
| 45 | else if(!strcasecmp(key, "multiplier")) { |
| 46 | *delay_multiplier = strtof(value, NULL); |
| 47 | if(isnan(*delay_multiplier) || isinf(*delay_multiplier) || islessequal(*delay_multiplier, 0)) { |
| 48 | netdata_log_error("Health configuration at line %zu of file '%s': invalid value '%s' for '%s' keyword", |
| 49 | line, filename, value, key); |
| 50 | } |
| 51 | else given_multiplier = 1; |
| 52 | } |
| 53 | else if(!strcasecmp(key, "max")) { |
| 54 | if (!duration_parse_seconds(value, delay_max_duration)) { |
| 55 | netdata_log_error("Health configuration at line %zu of file '%s': invalid value '%s' for '%s' keyword", |
| 56 | line, filename, value, key); |
| 57 | } |
| 58 | else given_max = 1; |
| 59 | } |
| 60 | else { |
| 61 | netdata_log_error("Health configuration at line %zu of file '%s': unknown keyword '%s'", |
| 62 | line, filename, key); |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | if(!given_up) |
| 67 | *delay_up_duration = 0; |
| 68 | |
| 69 | if(!given_down) |
| 70 | *delay_down_duration = 0; |
| 71 | |
| 72 | if(!given_multiplier) |
| 73 | *delay_multiplier = 1.0; |
| 74 | |
| 75 | if(!given_max) { |
| 76 | if((*delay_max_duration) < (*delay_up_duration) * (*delay_multiplier)) |
| 77 | *delay_max_duration = (int)((*delay_up_duration) * (*delay_multiplier)); |
| 78 | |
| 79 | if((*delay_max_duration) < (*delay_down_duration) * (*delay_multiplier)) |
| 80 | *delay_max_duration = (int)((*delay_down_duration) * (*delay_multiplier)); |
| 81 | } |
| 82 | |
| 83 | return 1; |
| 84 | } |
| 85 | |
| 86 | static inline ALERT_ACTION_OPTIONS health_parse_options(const char *s) { |
| 87 | ALERT_ACTION_OPTIONS options = ALERT_ACTION_OPTION_NONE; |
| 88 | char buf[100+1] = ""; |
| 89 | |
| 90 | while(*s) { |
| 91 | buf[0] = '\0'; |
| 92 | |
| 93 | // skip spaces |
| 94 | while(*s && isspace((uint8_t)*s)) |
| 95 | s++; |
| 96 | |
| 97 | // find the next space |
| 98 | size_t count = 0; |
| 99 | while(*s && count < 100 && !isspace((uint8_t)*s)) |
| 100 | buf[count++] = *s++; |
| 101 | |
| 102 | if(buf[0]) { |
| 103 | buf[count] = '\0'; |
| 104 | |
| 105 | if(!strcasecmp(buf, "no-clear-notification") || !strcasecmp(buf, "no-clear")) |
| 106 | options |= ALERT_ACTION_OPTION_NO_CLEAR_NOTIFICATION; |
| 107 | else |
| 108 | netdata_log_error("Ignoring unknown alarm option '%s'", buf); |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | return options; |
| 113 | } |
| 114 | |
| 115 | static inline int health_parse_repeat( |
| 116 | size_t line, |
| 117 | const char *file, |
| 118 | char *string, |
| 119 | uint32_t *warn_repeat_every, |
| 120 | uint32_t *crit_repeat_every |
| 121 | ) { |
| 122 | |
| 123 | char *s = string; |
| 124 | while(*s) { |
| 125 | char *key = s; |
| 126 | |
| 127 | while(*s && !isspace((uint8_t)*s)) s++; |
| 128 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 129 | |
| 130 | if(!*key) break; |
| 131 | |
| 132 | char *value = s; |
| 133 | while(*s && !isspace((uint8_t)*s)) s++; |
| 134 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 135 | |
| 136 | if(!strcasecmp(key, "off")) { |
| 137 | *warn_repeat_every = 0; |
| 138 | *crit_repeat_every = 0; |
| 139 | return 1; |
| 140 | } |
| 141 | if(!strcasecmp(key, "warning")) { |
| 142 | if (!duration_parse_seconds(value, (int *)warn_repeat_every)) { |
| 143 | netdata_log_error("Health configuration at line %zu of file '%s': invalid value '%s' for '%s' keyword", |
| 144 | line, file, value, key); |
| 145 | } |
| 146 | } |
| 147 | else if(!strcasecmp(key, "critical")) { |
| 148 | if (!duration_parse_seconds(value, (int *)crit_repeat_every)) { |
| 149 | netdata_log_error("Health configuration at line %zu of file '%s': invalid value '%s' for '%s' keyword", |
| 150 | line, file, value, key); |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | return 1; |
| 156 | } |
| 157 | |
| 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; |
| 162 | ac->before = 0; |
| 163 | ac->update_every = 0; |
| 164 | ac->options = 0; |
| 165 | ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_EQUAL; |
| 166 | ac->time_group_value = NAN; |
| 167 | |
| 168 | char *s = string, *key; |
| 169 | |
| 170 | // first is the group method |
| 171 | key = s; |
| 172 | while(*s && !isspace((uint8_t)*s) && *s != '(') s++; |
| 173 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 174 | if(!*s) { |
| 175 | netdata_log_error("Health configuration invalid chart calculation at line %zu of file '%s': expected group method followed by the 'after' time, but got '%s'", |
| 176 | line, filename, key); |
| 177 | return 0; |
| 178 | } |
| 179 | |
| 180 | bool group_options = false; |
| 181 | if(*s == '(') { |
| 182 | *s++ = '\0'; |
| 183 | group_options = true; |
| 184 | } |
| 185 | |
| 186 | if((ac->time_group = time_grouping_parse(key, RRDR_GROUPING_UNDEFINED)) == RRDR_GROUPING_UNDEFINED) { |
| 187 | netdata_log_error("Health configuration at line %zu of file '%s': invalid group method '%s'", |
| 188 | line, filename, key); |
| 189 | return 0; |
| 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++; |
| 199 | ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL; |
| 200 | } |
| 201 | else if(*s == '<') { |
| 202 | s++; |
| 203 | if(*s == '>') { |
| 204 | s++; |
| 205 | ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_NOT_EQUAL; |
| 206 | } |
| 207 | else if(*s == '=') { |
| 208 | s++; |
| 209 | ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS_EQUAL; |
| 210 | } |
| 211 | else |
| 212 | ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_LESS; |
| 213 | } |
| 214 | else if(*s == '>') { |
| 215 | s++; |
| 216 | if(*s == '=') { |
| 217 | s++; |
| 218 | ac->time_group_condition = ALERT_LOOKUP_TIME_GROUP_CONDITION_GREATER_EQUAL; |
| 219 | } |
| 220 | else |
| 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 | |
| 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 | } |
| 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 | } |
| 261 | |
| 262 | s++; |
| 263 | } |
| 264 | |
| 265 | switch (ac->time_group) { |
| 266 | default: |
| 267 | break; |
| 268 | |
| 269 | case RRDR_GROUPING_COUNTIF: |
| 270 | if(isnan(ac->time_group_value)) |
| 271 | ac->time_group_value = 0; |
| 272 | break; |
| 273 | |
| 274 | case RRDR_GROUPING_TRIMMED_MEAN: |
| 275 | case RRDR_GROUPING_TRIMMED_MEDIAN: |
| 276 | if(isnan(ac->time_group_value)) |
| 277 | ac->time_group_value = 5; |
| 278 | break; |
| 279 | |
| 280 | case RRDR_GROUPING_PERCENTILE: |
| 281 | if(isnan(ac->time_group_value)) |
| 282 | ac->time_group_value = 95; |
| 283 | break; |
| 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'; |
| 291 | |
| 292 | if(!duration_parse_seconds(key, &ac->after)) { |
| 293 | netdata_log_error("Health configuration at line %zu of file '%s': invalid duration '%s' after group method", |
| 294 | line, filename, key); |
| 295 | return 0; |
| 296 | } |
| 297 | |
| 298 | // sane defaults |
| 299 | ac->update_every = ABS(ac->after); |
| 300 | |
| 301 | // now we may have optional parameters |
| 302 | while(*s) { |
| 303 | key = s; |
| 304 | while(*s && !isspace((uint8_t)*s)) s++; |
| 305 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 306 | if(!*key) break; |
| 307 | |
| 308 | if(!strcasecmp(key, "at")) { |
| 309 | char *value = s; |
| 310 | while(*s && !isspace((uint8_t)*s)) s++; |
| 311 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 312 | |
| 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)) { |
| 320 | char *value = s; |
| 321 | while(*s && !isspace((uint8_t)*s)) s++; |
| 322 | while(*s && isspace((uint8_t)*s)) *s++ = '\0'; |
| 323 | |
| 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")) { |
| 331 | ac->options |= RRDR_OPTION_ABSOLUTE; |
| 332 | } |
| 333 | else if(!strcasecmp(key, "min2max")) { |
| 334 | ac->options |= RRDR_OPTION_DIMS_MIN2MAX; |
| 335 | } |
| 336 | else if(!strcasecmp(key, "average")) { |
| 337 | ac->options |= RRDR_OPTION_DIMS_AVERAGE; |
| 338 | } |
| 339 | else if(!strcasecmp(key, "min")) { |
| 340 | ac->options |= RRDR_OPTION_DIMS_MIN; |
| 341 | } |
| 342 | else if(!strcasecmp(key, "max")) { |
| 343 | ac->options |= RRDR_OPTION_DIMS_MAX; |
| 344 | } |
| 345 | else if(!strcasecmp(key, "sum")) { |
| 346 | ; |
| 347 | } |
| 348 | else if(!strcasecmp(key, "null2zero")) { |
| 349 | ac->options |= RRDR_OPTION_NULL2ZERO; |
| 350 | } |
| 351 | else if(!strcasecmp(key, "percentage")) { |
| 352 | ac->options |= RRDR_OPTION_PERCENTAGE; |
| 353 | } |
| 354 | else if(!strcasecmp(key, "unaligned")) { |
| 355 | ac->options |= RRDR_OPTION_NOT_ALIGNED; |
| 356 | } |
| 357 | else if(!strcasecmp(key, "anomaly-bit")) { |
| 358 | ac->options |= RRDR_OPTION_ANOMALY_BIT; |
| 359 | } |
| 360 | else if(!strcasecmp(key, "match-ids") || !strcasecmp(key, "match_ids")) { |
| 361 | ac->options |= RRDR_OPTION_MATCH_IDS; |
| 362 | } |
| 363 | else if(!strcasecmp(key, "match-names") || !strcasecmp(key, "match_names")) { |
| 364 | ac->options |= RRDR_OPTION_MATCH_NAMES; |
| 365 | } |
| 366 | else if(!strcasecmp(key, "of")) { |
| 367 | char *find = NULL; |
| 368 | if(*s && strcasecmp(s, "all") != 0) { |
| 369 | find = strcasestr(s, " foreach"); |
| 370 | if(find) { |
| 371 | *find = '\0'; |
| 372 | } |
| 373 | ac->dimensions = string_strdupz(s); |
| 374 | } |
| 375 | |
| 376 | if(!find) { |
| 377 | break; |
| 378 | } |
| 379 | s = ++find; |
| 380 | } |
| 381 | else { |
| 382 | netdata_log_error("Health configuration at line %zu of file '%s': unknown keyword '%s'", |
| 383 | line, filename, key); |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | return 1; |
| 388 | } |
| 389 | |
| 390 | static inline STRING *health_source_file(size_t line, const char *file) { |
| 391 | char buffer[FILENAME_MAX + 1]; |
| 392 | snprintfz(buffer, FILENAME_MAX, "line=%zu,file=%s", line, file); |
| 393 | return string_strdupz(buffer); |
| 394 | } |
| 395 | |
| 396 | char *health_edit_command_from_source(const char *source) |
| 397 | { |
| 398 | char buffer[FILENAME_MAX + 1]; |
| 399 | char *temp = strdupz(source); |
| 400 | char *line_num = strchr(temp, '@'); |
| 401 | char *line_p = temp; |
| 402 | char *file_no_path = strrchr(temp, '/'); |
| 403 | |
| 404 | // Check for the 'line=' format if '@' is not found |
| 405 | if (!line_num) { |
| 406 | line_num = strstr(temp, "line="); |
| 407 | file_no_path = strstr(temp, "file=/"); |
| 408 | } |
| 409 | |
| 410 | if (likely(file_no_path && line_num)) { |
| 411 | if (line_num == strchr(temp, '@')) { |
| 412 | *line_num = '\0'; // Handle the old format |
| 413 | } else { |
| 414 | line_num += strlen("line="); |
| 415 | file_no_path = strrchr(file_no_path + strlen("file="), '/'); |
| 416 | char *line_end = strchr(line_num, ','); |
| 417 | if (line_end) { |
| 418 | line_p = line_num; |
| 419 | *line_end = '\0'; |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | snprintfz( |
| 424 | buffer, |
| 425 | FILENAME_MAX, |
| 426 | "sudo %s/edit-config health.d/%s=%s=%s", |
| 427 | netdata_configured_user_config_dir, |
| 428 | file_no_path + 1, |
| 429 | line_p, |
| 430 | rrdhost_registry_hostname(localhost)); |
| 431 | } else { |
| 432 | buffer[0] = '\0'; |
| 433 | } |
| 434 | |
| 435 | freez(temp); |
| 436 | return strdupz(buffer); |
| 437 | } |
| 438 | |
| 439 | |
| 440 | static inline void strip_quotes(char *s) { |
| 441 | while(*s) { |
| 442 | if(*s == '\'' || *s == '"') *s = ' '; |
| 443 | s++; |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | static void replace_green_red(RRD_ALERT_PROTOTYPE *ap, NETDATA_DOUBLE green, NETDATA_DOUBLE red) { |
| 448 | if(!isnan(green)) { |
| 449 | STRING *green_str = string_strdupz("green"); |
| 450 | expression_hardcode_variable(ap->config.calculation, green_str, green); |
| 451 | expression_hardcode_variable(ap->config.warning, green_str, green); |
| 452 | expression_hardcode_variable(ap->config.critical, green_str, green); |
| 453 | string_freez(green_str); |
| 454 | } |
| 455 | |
| 456 | if(!isnan(red)) { |
| 457 | STRING *red_str = string_strdupz("red"); |
| 458 | expression_hardcode_variable(ap->config.calculation, red_str, red); |
| 459 | expression_hardcode_variable(ap->config.warning, red_str, red); |
| 460 | expression_hardcode_variable(ap->config.critical, red_str, red); |
| 461 | string_freez(red_str); |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | static void dims_grouping_from_rrdr_options(RRD_ALERT_PROTOTYPE *ap) { |
| 466 | if(ap->config.options & RRDR_OPTION_DIMS_MIN) |
| 467 | ap->config.dims_group = ALERT_LOOKUP_DIMS_MIN; |
| 468 | else if(ap->config.options & RRDR_OPTION_DIMS_MAX) |
| 469 | ap->config.dims_group = ALERT_LOOKUP_DIMS_MAX; |
| 470 | else if(ap->config.options & RRDR_OPTION_DIMS_MIN2MAX) |
| 471 | ap->config.dims_group = ALERT_LOOKUP_DIMS_MIN2MAX; |
| 472 | else if(ap->config.options & RRDR_OPTION_DIMS_AVERAGE) |
| 473 | ap->config.dims_group = ALERT_LOOKUP_DIMS_AVERAGE; |
| 474 | else |
| 475 | ap->config.dims_group = ALERT_LOOKUP_DIMS_SUM; |
| 476 | } |
| 477 | |
| 478 | static void lookup_data_source_from_rrdr_options(RRD_ALERT_PROTOTYPE *ap) { |
| 479 | if(ap->config.options & RRDR_OPTION_PERCENTAGE) |
| 480 | ap->config.data_source = ALERT_LOOKUP_DATA_SOURCE_PERCENTAGES; |
| 481 | else if(ap->config.options & RRDR_OPTION_ANOMALY_BIT) |
| 482 | ap->config.data_source = ALERT_LOOKUP_DATA_SOURCE_ANOMALIES; |
| 483 | else |
| 484 | ap->config.data_source = ALERT_LOOKUP_DATA_SOURCE_SAMPLES; |
| 485 | } |
| 486 | |
| 487 | #define PARSE_HEALTH_CONFIG_LOG_DUPLICATE_STRING_MSG(ax, member) do { \ |
| 488 | if(strcmp(string2str(ax->member), value) != 0) \ |
| 489 | netdata_log_error( \ |
| 490 | "Health configuration at line %zu of file '%s' for alarm '%s' has key '%s' twice, " \ |
| 491 | "once with value '%s' and later with value '%s'. Using ('%s').", \ |
| 492 | line, filename, string2str(ac->name), key, \ |
| 493 | string2str(ax->member), value, value); \ |
| 494 | } while(0) |
| 495 | |
| 496 | #define PARSE_HEALTH_CONFIG_LINE_STRING(ax, member) do { \ |
| 497 | if(ax->member) { \ |
| 498 | PARSE_HEALTH_CONFIG_LOG_DUPLICATE_STRING_MSG(ax, member); \ |
| 499 | string_freez(ax->member); \ |
| 500 | } \ |
| 501 | ax->member = string_strdupz(value); \ |
| 502 | } while(0) |
| 503 | |
| 504 | #define PARSE_HEALTH_CONFIG_LINE_PATTERN_APPEND(ax, member, label) do { \ |
| 505 | const char *_label = label; \ |
| 506 | if(_label && !*_label) \ |
| 507 | _label = NULL; \ |
| 508 | \ |
| 509 | if(value && (!*value || strcmp(value, "*") == 0)) \ |
| 510 | value = NULL; \ |
| 511 | else if(value && (strcmp(value, "!* *") == 0 || strcmp(value, "!*") == 0)) { \ |
| 512 | value = NULL; \ |
| 513 | ap->match.enabled = false; \ |
| 514 | } \ |
| 515 | \ |
| 516 | if(value && !_label && !strchr(value, '=')) { \ |
| 517 | netdata_log_error( \ |
| 518 | "Health configuration at line %zu of file '%s' for alarm '%s' has key '%s' " \ |
| 519 | "with value '%s' that does not match label=pattern. Ignoring it.", \ |
| 520 | line, filename, string2str(ac->name), key, value); \ |
| 521 | value = NULL; \ |
| 522 | } \ |
| 523 | \ |
| 524 | if(value) { \ |
| 525 | typeof(ax->member) _old = ax->member; \ |
| 526 | size_t _label_len = _label ? strlen(_label) : 0; \ |
| 527 | size_t _value_len = strlen(value); \ |
| 528 | size_t _old_len = _old ? string_strlen(_old) : 0; \ |
| 529 | size_t _buf_len = _label_len + (_label ? 1 : 0) + _value_len + (_old ? 1 : 0) + _old_len + 1; \ |
| 530 | char *_buf = mallocz(_buf_len); \ |
| 531 | snprintfz(_buf, _buf_len, "%s%s%s%s%s", \ |
| 532 | _label ? _label : "", \ |
| 533 | _label ? "=" : "", \ |
| 534 | value, \ |
| 535 | _old ? " " : "", \ |
| 536 | _old ? string2str(_old) : ""); \ |
| 537 | string_freez(_old); \ |
| 538 | ax->member = string_strdupz(_buf); \ |
| 539 | freez(_buf); \ |
| 540 | } \ |
| 541 | } while(0) |
| 542 | |
| 543 | int health_readfile(const char *filename, void *data __maybe_unused, bool stock_config) { |
| 544 | netdata_log_debug(D_HEALTH, "Health configuration reading file '%s'", filename); |
| 545 | |
| 546 | static uint32_t |
| 547 | hash_alarm = 0, |
| 548 | hash_template = 0, |
| 549 | hash_os = 0, |
| 550 | hash_on = 0, |
| 551 | hash_host = 0, |
| 552 | hash_plugin = 0, |
| 553 | hash_module = 0, |
| 554 | hash_calc = 0, |
| 555 | hash_green = 0, |
| 556 | hash_red = 0, |
| 557 | hash_warn = 0, |
| 558 | hash_crit = 0, |
| 559 | hash_exec = 0, |
| 560 | hash_every = 0, |
| 561 | hash_lookup = 0, |
| 562 | hash_units = 0, |
| 563 | hash_summary = 0, |
| 564 | hash_info = 0, |
| 565 | hash_class = 0, |
| 566 | hash_component = 0, |
| 567 | hash_type = 0, |
| 568 | hash_recipient = 0, |
| 569 | hash_delay = 0, |
| 570 | hash_options = 0, |
| 571 | hash_repeat = 0, |
| 572 | hash_host_label = 0, |
| 573 | hash_chart_label = 0; |
| 574 | |
| 575 | char buffer[HEALTH_CONF_MAX_LINE + 1]; |
| 576 | |
| 577 | if(unlikely(!hash_alarm)) { |
| 578 | hash_alarm = simple_uhash(HEALTH_ALARM_KEY); |
| 579 | hash_template = simple_uhash(HEALTH_TEMPLATE_KEY); |
| 580 | hash_on = simple_uhash(HEALTH_ON_KEY); |
| 581 | hash_os = simple_uhash(HEALTH_OS_KEY); |
| 582 | hash_host = simple_uhash(HEALTH_HOST_KEY); |
| 583 | hash_plugin = simple_uhash(HEALTH_PLUGIN_KEY); |
| 584 | hash_module = simple_uhash(HEALTH_MODULE_KEY); |
| 585 | hash_calc = simple_uhash(HEALTH_CALC_KEY); |
| 586 | hash_lookup = simple_uhash(HEALTH_LOOKUP_KEY); |
| 587 | hash_green = simple_uhash(HEALTH_GREEN_KEY); |
| 588 | hash_red = simple_uhash(HEALTH_RED_KEY); |
| 589 | hash_warn = simple_uhash(HEALTH_WARN_KEY); |
| 590 | hash_crit = simple_uhash(HEALTH_CRIT_KEY); |
| 591 | hash_exec = simple_uhash(HEALTH_EXEC_KEY); |
| 592 | hash_every = simple_uhash(HEALTH_EVERY_KEY); |
| 593 | hash_units = simple_hash(HEALTH_UNITS_KEY); |
| 594 | hash_summary = simple_hash(HEALTH_SUMMARY_KEY); |
| 595 | hash_info = simple_hash(HEALTH_INFO_KEY); |
| 596 | hash_class = simple_uhash(HEALTH_CLASS_KEY); |
| 597 | hash_component = simple_uhash(HEALTH_COMPONENT_KEY); |
| 598 | hash_type = simple_uhash(HEALTH_TYPE_KEY); |
| 599 | hash_recipient = simple_hash(HEALTH_RECIPIENT_KEY); |
| 600 | hash_delay = simple_uhash(HEALTH_DELAY_KEY); |
| 601 | hash_options = simple_uhash(HEALTH_OPTIONS_KEY); |
| 602 | hash_repeat = simple_uhash(HEALTH_REPEAT_KEY); |
| 603 | hash_host_label = simple_uhash(HEALTH_HOST_LABEL_KEY); |
| 604 | hash_chart_label = simple_uhash(HEALTH_CHART_LABEL_KEY); |
| 605 | } |
| 606 | |
| 607 | FILE *fp = fopen(filename, "r"); |
| 608 | if(!fp) { |
| 609 | netdata_log_error("Health configuration cannot read file '%s'.", filename); |
| 610 | return 0; |
| 611 | } |
| 612 | |
| 613 | RRD_ALERT_PROTOTYPE *ap = NULL; |
| 614 | struct rrd_alert_config *ac = NULL; |
| 615 | struct rrd_alert_match *am = NULL; |
| 616 | NETDATA_DOUBLE green = NAN; |
| 617 | NETDATA_DOUBLE red = NAN; |
| 618 | |
| 619 | size_t line = 0, append = 0; |
| 620 | char *s; |
| 621 | while((s = fgets(&buffer[append], (int)(HEALTH_CONF_MAX_LINE - append), fp)) || append) { |
| 622 | int stop_appending = !s; |
| 623 | line++; |
| 624 | s = trim(buffer); |
| 625 | if(!s || *s == '#') continue; |
| 626 | |
| 627 | append = strlen(s); |
| 628 | if(!stop_appending && s[append - 1] == '\\') { |
| 629 | s[append - 1] = ' '; |
| 630 | append = &s[append] - buffer; |
| 631 | if(append < HEALTH_CONF_MAX_LINE) |
| 632 | continue; |
| 633 | else |
| 634 | netdata_log_error( |
| 635 | "Health configuration has too long multi-line at line %zu of file '%s'.", |
| 636 | line, filename); |
| 637 | } |
| 638 | append = 0; |
| 639 | |
| 640 | char *key = s; |
| 641 | while(*s && *s != ':') s++; |
| 642 | if(!*s) { |
| 643 | netdata_log_error( |
| 644 | "Health configuration has invalid line %zu of file '%s'. It does not contain a ':'. Ignoring it.", |
| 645 | line, filename); |
| 646 | continue; |
| 647 | } |
| 648 | *s = '\0'; |
| 649 | s++; |
| 650 | |
| 651 | char *value = s; |
| 652 | key = trim_all(key); |
| 653 | value = trim_all(value); |
| 654 | |
| 655 | if(!key || !*key) { |
| 656 | netdata_log_error( |
| 657 | "Health configuration has invalid line %zu of file '%s'. Keyword is empty. Ignoring it.", |
| 658 | line, filename); |
| 659 | |
| 660 | continue; |
| 661 | } |
| 662 | |
| 663 | if(!value || !*value) { |
| 664 | netdata_log_error( |
| 665 | "Health configuration has invalid line %zu of file '%s'. value is empty. Ignoring it.", |
| 666 | line, filename); |
| 667 | continue; |
| 668 | } |
| 669 | |
| 670 | uint32_t hash = simple_uhash(key); |
| 671 | |
| 672 | if((hash == hash_alarm && !strcasecmp(key, HEALTH_ALARM_KEY)) || (hash == hash_template && !strcasecmp(key, HEALTH_TEMPLATE_KEY))) { |
| 673 | if(ap) { |
| 674 | lookup_data_source_from_rrdr_options(ap); |
| 675 | dims_grouping_from_rrdr_options(ap); |
| 676 | replace_green_red(ap, green, red); |
| 677 | health_prototype_add(ap, NULL); |
| 678 | freez(ap); |
| 679 | } |
| 680 | |
| 681 | ap = callocz(1, sizeof(*ap)); |
| 682 | am = &ap->match; |
| 683 | ac = &ap->config; |
| 684 | |
| 685 | { |
| 686 | char *tmp = strdupz(value); |
| 687 | if(rrdvar_fix_name(tmp)) |
| 688 | netdata_log_error("Health configuration renamed alarm '%s' to '%s'", value, tmp); |
| 689 | |
| 690 | ap->config.name = string_strdupz(tmp); |
| 691 | freez(tmp); |
| 692 | } |
| 693 | |
| 694 | ap->_internal.enabled = true; |
| 695 | ap->match.enabled = true; |
| 696 | ap->match.is_template = (hash == hash_template && !strcasecmp(key, HEALTH_TEMPLATE_KEY)); |
| 697 | ap->config.source = health_source_file(line, filename); |
| 698 | ap->config.source_type = stock_config ? DYNCFG_SOURCE_TYPE_STOCK : DYNCFG_SOURCE_TYPE_USER; |
| 699 | green = NAN; |
| 700 | red = NAN; |
| 701 | ap->config.delay_multiplier = 1; |
| 702 | ap->config.warn_repeat_every = health_globals.config.default_warn_repeat_every; |
| 703 | ap->config.crit_repeat_every = health_globals.config.default_crit_repeat_every; |
| 704 | } |
| 705 | else if(!am || !ac || !ap) { |
| 706 | netdata_log_error( |
| 707 | "Health configuration at line %zu of file '%s' has unknown key '%s'. " |
| 708 | "Expected either '" HEALTH_ALARM_KEY "' or '" HEALTH_TEMPLATE_KEY "'.", |
| 709 | line, filename, key); |
| 710 | } |
| 711 | else if(!am->is_template && hash == hash_on && !strcasecmp(key, HEALTH_ON_KEY)) { |
| 712 | PARSE_HEALTH_CONFIG_LINE_STRING(am, on.chart); |
| 713 | } |
| 714 | else if(am->is_template && hash == hash_on && !strcasecmp(key, HEALTH_ON_KEY)) { |
| 715 | PARSE_HEALTH_CONFIG_LINE_STRING(am, on.context); |
| 716 | } |
| 717 | else if(hash == hash_os && !strcasecmp(key, HEALTH_OS_KEY)) { |
| 718 | PARSE_HEALTH_CONFIG_LINE_PATTERN_APPEND(am, host_labels, "_os"); |
| 719 | } |
| 720 | else if(hash == hash_host && !strcasecmp(key, HEALTH_HOST_KEY)) { |
| 721 | PARSE_HEALTH_CONFIG_LINE_PATTERN_APPEND(am, host_labels, "_hostname"); |
| 722 | } |
| 723 | else if(hash == hash_host_label && !strcasecmp(key, HEALTH_HOST_LABEL_KEY)) { |
| 724 | PARSE_HEALTH_CONFIG_LINE_PATTERN_APPEND(am, host_labels, NULL); |
| 725 | } |
| 726 | else if(hash == hash_plugin && !strcasecmp(key, HEALTH_PLUGIN_KEY)) { |
| 727 | PARSE_HEALTH_CONFIG_LINE_PATTERN_APPEND(am, chart_labels, "_collect_plugin"); |
| 728 | } |
| 729 | else if(hash == hash_module && !strcasecmp(key, HEALTH_MODULE_KEY)) { |
| 730 | PARSE_HEALTH_CONFIG_LINE_PATTERN_APPEND(am, chart_labels, "_collect_module"); |
| 731 | } |
| 732 | else if(hash == hash_chart_label && !strcasecmp(key, HEALTH_CHART_LABEL_KEY)) { |
| 733 | PARSE_HEALTH_CONFIG_LINE_PATTERN_APPEND(am, chart_labels, NULL); |
| 734 | } |
| 735 | else if(hash == hash_class && !strcasecmp(key, HEALTH_CLASS_KEY)) { |
| 736 | strip_quotes(value); |
| 737 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, classification); |
| 738 | } |
| 739 | else if(hash == hash_component && !strcasecmp(key, HEALTH_COMPONENT_KEY)) { |
| 740 | strip_quotes(value); |
| 741 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, component); |
| 742 | } |
| 743 | else if(hash == hash_type && !strcasecmp(key, HEALTH_TYPE_KEY)) { |
| 744 | strip_quotes(value); |
| 745 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, type); |
| 746 | } |
| 747 | else if(hash == hash_lookup && !strcasecmp(key, HEALTH_LOOKUP_KEY)) { |
| 748 | health_parse_db_lookup(line, filename, value, ac); |
| 749 | } |
| 750 | else if(hash == hash_every && !strcasecmp(key, HEALTH_EVERY_KEY)) { |
| 751 | if(!duration_parse_seconds(value, &ac->update_every)) |
| 752 | netdata_log_error( |
| 753 | "Health configuration at line %zu of file '%s' for alarm '%s' at key '%s' " |
| 754 | "cannot parse duration: '%s'.", |
| 755 | line, filename, string2str(ac->name), key, value); |
| 756 | } |
| 757 | else if(hash == hash_green && !strcasecmp(key, HEALTH_GREEN_KEY)) { |
| 758 | char *e; |
| 759 | green = str2ndd(value, &e); |
| 760 | if(e && *e) { |
| 761 | netdata_log_error( |
| 762 | "Health configuration at line %zu of file '%s' for alarm '%s' at key '%s' " |
| 763 | "leaves this string unmatched: '%s'.", |
| 764 | line, filename, string2str(ac->name), key, e); |
| 765 | } |
| 766 | } |
| 767 | else if(hash == hash_red && !strcasecmp(key, HEALTH_RED_KEY)) { |
| 768 | char *e; |
| 769 | red = str2ndd(value, &e); |
| 770 | if(e && *e) { |
| 771 | netdata_log_error( |
| 772 | "Health configuration at line %zu of file '%s' for alarm '%s' at key '%s' " |
| 773 | "leaves this string unmatched: '%s'.", |
| 774 | line, filename, string2str(ac->name), key, e); |
| 775 | } |
| 776 | } |
| 777 | else if(hash == hash_calc && !strcasecmp(key, HEALTH_CALC_KEY)) { |
| 778 | const char *failed_at = NULL; |
| 779 | int error = 0; |
| 780 | ac->calculation = expression_parse(value, &failed_at, &error); |
| 781 | if(!ac->calculation) { |
| 782 | netdata_log_error( |
| 783 | "Health configuration at line %zu of file '%s' for alarm '%s' at key '%s' " |
| 784 | "has non-parseable expression '%s': %s at '%s'", |
| 785 | line, filename, string2str(ac->name), key, value, expression_strerror(error), failed_at); |
| 786 | am->enabled = false; |
| 787 | } |
| 788 | } |
| 789 | else if(hash == hash_warn && !strcasecmp(key, HEALTH_WARN_KEY)) { |
| 790 | const char *failed_at = NULL; |
| 791 | int error = 0; |
| 792 | ac->warning = expression_parse(value, &failed_at, &error); |
| 793 | if(!ac->warning) { |
| 794 | netdata_log_error( |
| 795 | "Health configuration at line %zu of file '%s' for alarm '%s' at key '%s' " |
| 796 | "has non-parseable expression '%s': %s at '%s'", |
| 797 | line, filename, string2str(ac->name), key, value, expression_strerror(error), failed_at); |
| 798 | am->enabled = false; |
| 799 | } |
| 800 | } |
| 801 | else if(hash == hash_crit && !strcasecmp(key, HEALTH_CRIT_KEY)) { |
| 802 | const char *failed_at = NULL; |
| 803 | int error = 0; |
| 804 | ac->critical = expression_parse(value, &failed_at, &error); |
| 805 | if(!ac->critical) { |
| 806 | netdata_log_error( |
| 807 | "Health configuration at line %zu of file '%s' for alarm '%s' at key '%s' " |
| 808 | "has non-parseable expression '%s': %s at '%s'", |
| 809 | line, filename, string2str(ac->name), key, value, expression_strerror(error), failed_at); |
| 810 | am->enabled = false; |
| 811 | } |
| 812 | } |
| 813 | else if(hash == hash_exec && !strcasecmp(key, HEALTH_EXEC_KEY)) { |
| 814 | strip_quotes(value); |
| 815 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, exec); |
| 816 | } |
| 817 | else if(hash == hash_recipient && !strcasecmp(key, HEALTH_RECIPIENT_KEY)) { |
| 818 | strip_quotes(value); |
| 819 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, recipient); |
| 820 | } |
| 821 | else if(hash == hash_units && !strcasecmp(key, HEALTH_UNITS_KEY)) { |
| 822 | strip_quotes(value); |
| 823 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, units); |
| 824 | } |
| 825 | else if(hash == hash_summary && !strcasecmp(key, HEALTH_SUMMARY_KEY)) { |
| 826 | strip_quotes(value); |
| 827 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, summary); |
| 828 | } |
| 829 | else if(hash == hash_info && !strcasecmp(key, HEALTH_INFO_KEY)) { |
| 830 | strip_quotes(value); |
| 831 | PARSE_HEALTH_CONFIG_LINE_STRING(ac, info); |
| 832 | } |
| 833 | else if(hash == hash_delay && !strcasecmp(key, HEALTH_DELAY_KEY)) { |
| 834 | health_parse_delay(line, filename, value, |
| 835 | &ac->delay_up_duration, &ac->delay_down_duration, |
| 836 | &ac->delay_max_duration, &ac->delay_multiplier); |
| 837 | } |
| 838 | else if(hash == hash_options && !strcasecmp(key, HEALTH_OPTIONS_KEY)) { |
| 839 | ac->alert_action_options |= health_parse_options(value); |
| 840 | } |
| 841 | else if(hash == hash_repeat && !strcasecmp(key, HEALTH_REPEAT_KEY)){ |
| 842 | health_parse_repeat(line, filename, value, |
| 843 | &ac->warn_repeat_every, |
| 844 | &ac->crit_repeat_every); |
| 845 | ac->has_custom_repeat_config = true; |
| 846 | } |
| 847 | else { |
| 848 | if (strcmp(key, "families") != 0 && strcmp(key, "charts") != 0) |
| 849 | netdata_log_error( |
| 850 | "Health configuration at line %zu of file '%s' for alarm/template '%s' has unknown key '%s'.", |
| 851 | line, filename, string2str(ac->name), key); |
| 852 | } |
| 853 | } |
| 854 | |
| 855 | if(ap) { |
| 856 | lookup_data_source_from_rrdr_options(ap); |
| 857 | dims_grouping_from_rrdr_options(ap); |
| 858 | replace_green_red(ap, green, red); |
| 859 | health_prototype_add(ap, NULL); |
| 860 | freez(ap); |
| 861 | } |
| 862 | |
| 863 | fclose(fp); |
| 864 | return 1; |
| 865 | } |