| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "log2journal.h" |
| 4 | |
| 5 | // ---------------------------------------------------------------------------- |
| 6 | |
| 7 | void log_job_init(LOG_JOB *jb) { |
| 8 | memset(jb, 0, sizeof(*jb)); |
| 9 | simple_hashtable_init_KEY(&jb->hashtable, 32); |
| 10 | hashed_key_set(&jb->line.key, "LINE", -1); |
| 11 | } |
| 12 | |
| 13 | static void simple_hashtable_cleanup_allocated_keys(SIMPLE_HASHTABLE_KEY *ht) { |
| 14 | SIMPLE_HASHTABLE_FOREACH_READ_ONLY(ht, sl, _KEY) { |
| 15 | HASHED_KEY *k = SIMPLE_HASHTABLE_FOREACH_READ_ONLY_VALUE(sl); |
| 16 | if(k && k->flags & HK_HASHTABLE_ALLOCATED) { |
| 17 | // the order of these statements is important! |
| 18 | simple_hashtable_del_slot_KEY(ht, sl); // remove any references to n |
| 19 | hashed_key_cleanup(k); // cleanup the internals of n |
| 20 | freez(k); // free n |
| 21 | } |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | void log_job_cleanup(LOG_JOB *jb) { |
| 26 | hashed_key_cleanup(&jb->line.key); |
| 27 | |
| 28 | if(jb->prefix) { |
| 29 | freez((void *) jb->prefix); |
| 30 | jb->prefix = NULL; |
| 31 | } |
| 32 | |
| 33 | if(jb->pattern) { |
| 34 | freez((void *) jb->pattern); |
| 35 | jb->pattern = NULL; |
| 36 | } |
| 37 | |
| 38 | for(size_t i = 0; i < jb->injections.used ;i++) |
| 39 | injection_cleanup(&jb->injections.keys[i]); |
| 40 | |
| 41 | for(size_t i = 0; i < jb->unmatched.injections.used ;i++) |
| 42 | injection_cleanup(&jb->unmatched.injections.keys[i]); |
| 43 | |
| 44 | for(size_t i = 0; i < jb->renames.used ;i++) |
| 45 | rename_cleanup(&jb->renames.array[i]); |
| 46 | |
| 47 | for(size_t i = 0; i < jb->rewrites.used; i++) |
| 48 | rewrite_cleanup(&jb->rewrites.array[i]); |
| 49 | |
| 50 | search_pattern_cleanup(&jb->filter.include); |
| 51 | search_pattern_cleanup(&jb->filter.exclude); |
| 52 | |
| 53 | hashed_key_cleanup(&jb->filename.key); |
| 54 | hashed_key_cleanup(&jb->unmatched.key); |
| 55 | |
| 56 | txt_l2j_cleanup(&jb->rewrites.tmp); |
| 57 | txt_l2j_cleanup(&jb->filename.current); |
| 58 | |
| 59 | simple_hashtable_cleanup_allocated_keys(&jb->hashtable); |
| 60 | simple_hashtable_destroy_KEY(&jb->hashtable); |
| 61 | |
| 62 | // remove references to everything else, to reveal them in valgrind |
| 63 | memset(jb, 0, sizeof(*jb)); |
| 64 | } |
| 65 | |
| 66 | // ---------------------------------------------------------------------------- |
| 67 | |
| 68 | bool log_job_filename_key_set(LOG_JOB *jb, const char *key, size_t key_len) { |
| 69 | if(!key || !*key) { |
| 70 | l2j_log("filename key cannot be empty."); |
| 71 | return false; |
| 72 | } |
| 73 | |
| 74 | hashed_key_set(&jb->filename.key, key, key_len); |
| 75 | |
| 76 | return true; |
| 77 | } |
| 78 | |
| 79 | bool log_job_key_prefix_set(LOG_JOB *jb, const char *prefix, size_t prefix_len) { |
| 80 | if(!prefix || !*prefix) { |
| 81 | l2j_log("filename key cannot be empty."); |
| 82 | return false; |
| 83 | } |
| 84 | |
| 85 | if(jb->prefix) |
| 86 | freez((char*)jb->prefix); |
| 87 | |
| 88 | jb->prefix = strndupz(prefix, prefix_len); |
| 89 | |
| 90 | return true; |
| 91 | } |
| 92 | |
| 93 | bool log_job_pattern_set(LOG_JOB *jb, const char *pattern, size_t pattern_len) { |
| 94 | if(!pattern || !*pattern) { |
| 95 | l2j_log("filename key cannot be empty."); |
| 96 | return false; |
| 97 | } |
| 98 | |
| 99 | if(jb->pattern) |
| 100 | freez((char*)jb->pattern); |
| 101 | |
| 102 | jb->pattern = strndupz(pattern, pattern_len); |
| 103 | |
| 104 | return true; |
| 105 | } |
| 106 | |
| 107 | bool log_job_include_pattern_set(LOG_JOB *jb, const char *pattern, size_t pattern_len) { |
| 108 | if(jb->filter.include.re) { |
| 109 | l2j_log("FILTER INCLUDE: there is already an include filter set"); |
| 110 | return false; |
| 111 | } |
| 112 | |
| 113 | if(!search_pattern_set(&jb->filter.include, pattern, pattern_len)) { |
| 114 | l2j_log("FILTER INCLUDE: failed: %s", jb->filter.include.error.txt); |
| 115 | return false; |
| 116 | } |
| 117 | |
| 118 | return true; |
| 119 | } |
| 120 | |
| 121 | bool log_job_exclude_pattern_set(LOG_JOB *jb, const char *pattern, size_t pattern_len) { |
| 122 | if(jb->filter.exclude.re) { |
| 123 | l2j_log("FILTER INCLUDE: there is already an exclude filter set"); |
| 124 | return false; |
| 125 | } |
| 126 | |
| 127 | if(!search_pattern_set(&jb->filter.exclude, pattern, pattern_len)) { |
| 128 | l2j_log("FILTER EXCLUDE: failed: %s", jb->filter.exclude.error.txt); |
| 129 | return false; |
| 130 | } |
| 131 | |
| 132 | return true; |
| 133 | } |
| 134 | |
| 135 | // ---------------------------------------------------------------------------- |
| 136 | |
| 137 | static bool parse_rename(LOG_JOB *jb, const char *param) { |
| 138 | // Search for '=' in param |
| 139 | const char *equal_sign = strchr(param, '='); |
| 140 | if (!equal_sign || equal_sign == param) { |
| 141 | l2j_log("Error: Invalid rename format, '=' not found in %s", param); |
| 142 | return false; |
| 143 | } |
| 144 | |
| 145 | const char *new_key = param; |
| 146 | size_t new_key_len = equal_sign - new_key; |
| 147 | |
| 148 | const char *old_key = equal_sign + 1; |
| 149 | size_t old_key_len = strlen(old_key); |
| 150 | |
| 151 | return log_job_rename_add(jb, new_key, new_key_len, old_key, old_key_len); |
| 152 | } |
| 153 | |
| 154 | static bool is_symbol(char c) { |
| 155 | return !isalpha(c) && !isdigit(c) && !iscntrl(c); |
| 156 | } |
| 157 | |
| 158 | struct { |
| 159 | const char *keyword; |
| 160 | int action; |
| 161 | RW_FLAGS flag; |
| 162 | } rewrite_flags[] = { |
| 163 | {"match", 1, RW_MATCH_PCRE2}, |
| 164 | {"match", 0, RW_MATCH_NON_EMPTY}, |
| 165 | |
| 166 | {"regex", 1, RW_MATCH_PCRE2}, |
| 167 | {"regex", 0, RW_MATCH_NON_EMPTY}, |
| 168 | |
| 169 | {"pcre2", 1, RW_MATCH_PCRE2}, |
| 170 | {"pcre2", 0, RW_MATCH_NON_EMPTY}, |
| 171 | |
| 172 | {"non_empty", 1, RW_MATCH_NON_EMPTY}, |
| 173 | {"non_empty", 0, RW_MATCH_PCRE2}, |
| 174 | |
| 175 | {"non-empty", 1, RW_MATCH_NON_EMPTY}, |
| 176 | {"non-empty", 0, RW_MATCH_PCRE2}, |
| 177 | |
| 178 | {"not_empty", 1, RW_MATCH_NON_EMPTY}, |
| 179 | {"not_empty", 0, RW_MATCH_PCRE2}, |
| 180 | |
| 181 | {"not-empty", 1, RW_MATCH_NON_EMPTY}, |
| 182 | {"not-empty", 0, RW_MATCH_PCRE2}, |
| 183 | |
| 184 | {"stop", 0, RW_DONT_STOP}, |
| 185 | {"no-stop", 1, RW_DONT_STOP}, |
| 186 | {"no_stop", 1, RW_DONT_STOP}, |
| 187 | {"dont-stop", 1, RW_DONT_STOP}, |
| 188 | {"dont_stop", 1, RW_DONT_STOP}, |
| 189 | {"continue", 1, RW_DONT_STOP}, |
| 190 | {"inject", 1, RW_INJECT}, |
| 191 | {"existing", 0, RW_INJECT}, |
| 192 | }; |
| 193 | |
| 194 | RW_FLAGS parse_rewrite_flags(const char *options) { |
| 195 | RW_FLAGS flags = RW_MATCH_PCRE2; // Default option |
| 196 | |
| 197 | // Tokenize the input options using "," |
| 198 | char *token; |
| 199 | char *optionsCopy = strdup(options); // Make a copy to avoid modifying the original |
| 200 | token = strtok(optionsCopy, ","); |
| 201 | |
| 202 | while (token != NULL) { |
| 203 | // Find the keyword-action mapping |
| 204 | bool found = false; |
| 205 | |
| 206 | for (size_t i = 0; i < sizeof(rewrite_flags) / sizeof(rewrite_flags[0]); i++) { |
| 207 | if (strcmp(token, rewrite_flags[i].keyword) == 0) { |
| 208 | if (rewrite_flags[i].action == 1) { |
| 209 | flags |= rewrite_flags[i].flag; // Set the flag |
| 210 | } else { |
| 211 | flags &= ~rewrite_flags[i].flag; // Unset the flag |
| 212 | } |
| 213 | |
| 214 | found = true; |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | if(!found) |
| 219 | l2j_log("Warning: rewrite options '%s' is not understood.", token); |
| 220 | |
| 221 | // Get the next token |
| 222 | token = strtok(NULL, ","); |
| 223 | } |
| 224 | |
| 225 | free(optionsCopy); // Free the copied string |
| 226 | |
| 227 | return flags; |
| 228 | } |
| 229 | |
| 230 | |
| 231 | static bool parse_rewrite(LOG_JOB *jb, const char *param) { |
| 232 | // Search for '=' in param |
| 233 | const char *equal_sign = strchr(param, '='); |
| 234 | if (!equal_sign || equal_sign == param) { |
| 235 | l2j_log("Error: Invalid rewrite format, '=' not found in %s", param); |
| 236 | return false; |
| 237 | } |
| 238 | |
| 239 | // Get the next character as the separator |
| 240 | char separator = *(equal_sign + 1); |
| 241 | if (!separator || !is_symbol(separator)) { |
| 242 | l2j_log("Error: rewrite separator not found after '=', or is not one of /\\|-# in: %s", param); |
| 243 | return false; |
| 244 | } |
| 245 | |
| 246 | // Find the next occurrence of the separator |
| 247 | const char *second_separator = strchr(equal_sign + 2, separator); |
| 248 | if (!second_separator) { |
| 249 | l2j_log("Error: rewrite second separator not found in: %s", param); |
| 250 | return false; |
| 251 | } |
| 252 | |
| 253 | // Check if the search pattern is empty |
| 254 | if (equal_sign + 1 == second_separator) { |
| 255 | l2j_log("Error: rewrite search pattern is empty in: %s", param); |
| 256 | return false; |
| 257 | } |
| 258 | |
| 259 | // Check if the replacement pattern is empty |
| 260 | if (*(second_separator + 1) == '\0') { |
| 261 | l2j_log("Error: rewrite replacement pattern is empty in: %s", param); |
| 262 | return false; |
| 263 | } |
| 264 | |
| 265 | RW_FLAGS flags = RW_MATCH_PCRE2; |
| 266 | const char *third_separator = strchr(second_separator + 1, separator); |
| 267 | if(third_separator) |
| 268 | flags = parse_rewrite_flags(third_separator + 1); |
| 269 | |
| 270 | // Extract key, search pattern, and replacement pattern |
| 271 | char *key = strndupz(param, equal_sign - param); |
| 272 | char *search_pattern = strndupz(equal_sign + 2, second_separator - (equal_sign + 2)); |
| 273 | char *replace_pattern = third_separator ? strndup(second_separator + 1, third_separator - (second_separator + 1)) : strdupz(second_separator + 1); |
| 274 | |
| 275 | if(!*search_pattern) |
| 276 | flags &= ~RW_MATCH_PCRE2; |
| 277 | |
| 278 | bool ret = log_job_rewrite_add(jb, key, flags, search_pattern, replace_pattern); |
| 279 | |
| 280 | freez(key); |
| 281 | freez(search_pattern); |
| 282 | freez(replace_pattern); |
| 283 | |
| 284 | return ret; |
| 285 | } |
| 286 | |
| 287 | static bool parse_inject(LOG_JOB *jb, const char *value, bool unmatched) { |
| 288 | const char *equal = strchr(value, '='); |
| 289 | if (!equal) { |
| 290 | l2j_log("Error: injection '%s' does not have an equal sign.", value); |
| 291 | return false; |
| 292 | } |
| 293 | |
| 294 | const char *key = value; |
| 295 | const char *val = equal + 1; |
| 296 | log_job_injection_add(jb, key, equal - key, val, strlen(val), unmatched); |
| 297 | |
| 298 | return true; |
| 299 | } |
| 300 | |
| 301 | bool log_job_command_line_parse_parameters(LOG_JOB *jb, int argc, char **argv) { |
| 302 | for (int i = 1; i < argc; i++) { |
| 303 | char *arg = argv[i]; |
| 304 | if (strcmp(arg, "--help") == 0 || strcmp(arg, "-h") == 0) { |
| 305 | log_job_command_line_help(argv[0]); |
| 306 | exit(0); |
| 307 | } |
| 308 | #if defined(NETDATA_DEV_MODE) || defined(NETDATA_INTERNAL_CHECKS) |
| 309 | else if(strcmp(arg, "--test") == 0) { |
| 310 | // logfmt_test(); |
| 311 | json_test(); |
| 312 | exit(1); |
| 313 | } |
| 314 | #endif |
| 315 | else if (strcmp(arg, "--show-config") == 0) { |
| 316 | jb->show_config = true; |
| 317 | } |
| 318 | else { |
| 319 | char buffer[1024]; |
| 320 | char *param = NULL; |
| 321 | char *value = NULL; |
| 322 | |
| 323 | char *equal_sign = strchr(arg, '='); |
| 324 | if (equal_sign) { |
| 325 | copy_to_buffer(buffer, sizeof(buffer), arg, equal_sign - arg); |
| 326 | param = buffer; |
| 327 | value = equal_sign + 1; |
| 328 | } |
| 329 | else { |
| 330 | param = arg; |
| 331 | if (i + 1 < argc) { |
| 332 | value = argv[++i]; |
| 333 | } |
| 334 | else { |
| 335 | if (!jb->pattern) { |
| 336 | log_job_pattern_set(jb, arg, strlen(arg)); |
| 337 | continue; |
| 338 | } else { |
| 339 | l2j_log( |
| 340 | "Error: Multiple patterns detected. Specify only one pattern. The first is '%s', the second is '%s'", |
| 341 | jb->pattern, |
| 342 | arg); |
| 343 | return false; |
| 344 | } |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | if (strcmp(param, "--filename-key") == 0) { |
| 349 | if(!log_job_filename_key_set(jb, value, value ? strlen(value) : 0)) |
| 350 | return false; |
| 351 | } |
| 352 | else if (strcmp(param, "--prefix") == 0) { |
| 353 | if(!log_job_key_prefix_set(jb, value, value ? strlen(value) : 0)) |
| 354 | return false; |
| 355 | } |
| 356 | #ifdef HAVE_LIBYAML |
| 357 | else if (strcmp(param, "-f") == 0 || strcmp(param, "--file") == 0) { |
| 358 | if (!yaml_parse_file(value, jb)) |
| 359 | return false; |
| 360 | } |
| 361 | else if (strcmp(param, "-c") == 0 || strcmp(param, "--config") == 0) { |
| 362 | if (!yaml_parse_config(value, jb)) |
| 363 | return false; |
| 364 | } |
| 365 | #endif |
| 366 | else if (strcmp(param, "--unmatched-key") == 0) |
| 367 | hashed_key_set(&jb->unmatched.key, value, -1); |
| 368 | else if (strcmp(param, "--inject") == 0) { |
| 369 | if (!parse_inject(jb, value, false)) |
| 370 | return false; |
| 371 | } |
| 372 | else if (strcmp(param, "--inject-unmatched") == 0) { |
| 373 | if (!parse_inject(jb, value, true)) |
| 374 | return false; |
| 375 | } |
| 376 | else if (strcmp(param, "--rewrite") == 0) { |
| 377 | if (!parse_rewrite(jb, value)) |
| 378 | return false; |
| 379 | } |
| 380 | else if (strcmp(param, "--rename") == 0) { |
| 381 | if (!parse_rename(jb, value)) |
| 382 | return false; |
| 383 | } |
| 384 | else if (strcmp(param, "--include") == 0) { |
| 385 | if (!log_job_include_pattern_set(jb, value, strlen(value))) |
| 386 | return false; |
| 387 | } |
| 388 | else if (strcmp(param, "--exclude") == 0) { |
| 389 | if (!log_job_exclude_pattern_set(jb, value, strlen(value))) |
| 390 | return false; |
| 391 | } |
| 392 | else { |
| 393 | i--; |
| 394 | if (!jb->pattern) { |
| 395 | log_job_pattern_set(jb, arg, strlen(arg)); |
| 396 | continue; |
| 397 | } else { |
| 398 | l2j_log( |
| 399 | "Error: Multiple patterns detected. Specify only one pattern. The first is '%s', the second is '%s'", |
| 400 | jb->pattern, |
| 401 | arg); |
| 402 | return false; |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | } |
| 407 | |
| 408 | // Check if a pattern is set and exactly one pattern is specified |
| 409 | if (!jb->pattern) { |
| 410 | l2j_log("Warning: pattern not specified. Try the default config with: -c default"); |
| 411 | log_job_command_line_help(argv[0]); |
| 412 | return false; |
| 413 | } |
| 414 | |
| 415 | return true; |
| 416 | } |