| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "log2journal.h" |
| 4 | #include "libnetdata/json/json-c-parser-inline.h" |
| 5 | #include "libnetdata/yaml/yaml.h" |
| 6 | |
| 7 | // ---------------------------------------------------------------------------- |
| 8 | // yaml configuration file |
| 9 | |
| 10 | #ifdef HAVE_LIBYAML |
| 11 | |
| 12 | // ---------------------------------------------------------------------------- |
| 13 | // JSON-C based YAML parsing using libnetdata's YAML parser |
| 14 | |
| 15 | static bool log2journal_config_from_json(json_object *jobj, void *data, BUFFER *error) { |
| 16 | char path[1024]; path[0] = '\0'; |
| 17 | LOG_JOB *jb = data; |
| 18 | |
| 19 | // Parse pattern (optional - despite being conceptually required, we handle it gracefully) |
| 20 | CLEAN_CHAR_P *pattern = NULL; |
| 21 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "pattern", pattern, error, JSONC_OPTIONAL); |
| 22 | if(pattern) { |
| 23 | if(!log_job_pattern_set(jb, pattern, strlen(pattern))) { |
| 24 | buffer_sprintf(error, "failed to set pattern"); |
| 25 | return false; |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // Parse prefix (optional) |
| 30 | CLEAN_CHAR_P *prefix = NULL; |
| 31 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "prefix", prefix, error, JSONC_OPTIONAL); |
| 32 | if(prefix) { |
| 33 | if(!log_job_key_prefix_set(jb, prefix, strlen(prefix))) { |
| 34 | buffer_sprintf(error, "failed to set prefix"); |
| 35 | return false; |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | // Parse filename injection (optional) |
| 40 | JSONC_PARSE_SUBOBJECT(jobj, path, "filename", error, JSONC_STRICT, { |
| 41 | CLEAN_CHAR_P *key = NULL; |
| 42 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "key", key, error, JSONC_REQUIRED); |
| 43 | if(key) { |
| 44 | if(!log_job_filename_key_set(jb, key, strlen(key))) { |
| 45 | buffer_sprintf(error, "failed to set filename key"); |
| 46 | return false; |
| 47 | } |
| 48 | } |
| 49 | }); |
| 50 | |
| 51 | // Parse filter (optional) |
| 52 | JSONC_PARSE_SUBOBJECT(jobj, path, "filter", error, JSONC_STRICT, { |
| 53 | CLEAN_CHAR_P *include = NULL; |
| 54 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "include", include, error, JSONC_OPTIONAL); |
| 55 | if(include) { |
| 56 | if(!log_job_include_pattern_set(jb, include, strlen(include))) { |
| 57 | buffer_sprintf(error, "failed to set include pattern"); |
| 58 | return false; |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | CLEAN_CHAR_P *exclude = NULL; |
| 63 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "exclude", exclude, error, JSONC_OPTIONAL); |
| 64 | if(exclude) { |
| 65 | if(!log_job_exclude_pattern_set(jb, exclude, strlen(exclude))) { |
| 66 | buffer_sprintf(error, "failed to set exclude pattern"); |
| 67 | return false; |
| 68 | } |
| 69 | } |
| 70 | }); |
| 71 | |
| 72 | // Parse injections array (optional) |
| 73 | JSONC_PARSE_ARRAY(jobj, path, "inject", error, JSONC_STRICT, { |
| 74 | size_t i; |
| 75 | JSONC_PARSE_ARRAY_ITEM_OBJECT(jobj, path, i, JSONC_REQUIRED, { |
| 76 | CLEAN_CHAR_P *key = NULL; |
| 77 | CLEAN_CHAR_P *value = NULL; |
| 78 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "key", key, error, JSONC_REQUIRED); |
| 79 | JSONC_PARSE_SCALAR2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "value", value, error, JSONC_REQUIRED); |
| 80 | if(key && value) { |
| 81 | if(!log_job_injection_add(jb, key, strlen(key), value, strlen(value), false)) { |
| 82 | buffer_sprintf(error, "failed to add injection for '%s.inject'", path); |
| 83 | return false; |
| 84 | } |
| 85 | } |
| 86 | }); |
| 87 | }); |
| 88 | |
| 89 | // Parse rename array (optional) |
| 90 | JSONC_PARSE_ARRAY(jobj, path, "rename", error, JSONC_STRICT, { |
| 91 | size_t i; |
| 92 | JSONC_PARSE_ARRAY_ITEM_OBJECT(jobj, path, i, JSONC_REQUIRED, { |
| 93 | CLEAN_CHAR_P *new_key = NULL; |
| 94 | CLEAN_CHAR_P *old_key = NULL; |
| 95 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "new_key", new_key, error, JSONC_REQUIRED); |
| 96 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "old_key", old_key, error, JSONC_REQUIRED); |
| 97 | if(new_key && old_key) { |
| 98 | if(!log_job_rename_add(jb, new_key, strlen(new_key), old_key, strlen(old_key))) { |
| 99 | buffer_sprintf(error, "failed to add rename for '%s.rename'", path); |
| 100 | return false; |
| 101 | } |
| 102 | } |
| 103 | }); |
| 104 | }); |
| 105 | |
| 106 | // Parse rewrite array (optional) |
| 107 | JSONC_PARSE_ARRAY(jobj, path, "rewrite", error, JSONC_STRICT, { |
| 108 | size_t i; |
| 109 | JSONC_PARSE_ARRAY_ITEM_OBJECT(jobj, path, i, JSONC_REQUIRED, { |
| 110 | CLEAN_CHAR_P *key = NULL; |
| 111 | CLEAN_CHAR_P *match = NULL; |
| 112 | CLEAN_CHAR_P *not_empty = NULL; |
| 113 | CLEAN_CHAR_P *value = NULL; |
| 114 | RW_FLAGS flags = RW_NONE; |
| 115 | |
| 116 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "key", key, error, JSONC_REQUIRED); |
| 117 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "match", match, error, JSONC_OPTIONAL); |
| 118 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "not_empty", not_empty, error, JSONC_OPTIONAL); |
| 119 | JSONC_PARSE_SCALAR2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "value", value, error, JSONC_REQUIRED); |
| 120 | |
| 121 | bool stop = true; |
| 122 | bool inject = false; |
| 123 | JSONC_PARSE_BOOL_OR_ERROR_AND_RETURN(jobj, path, "stop", stop, error, JSONC_OPTIONAL); |
| 124 | JSONC_PARSE_BOOL_OR_ERROR_AND_RETURN(jobj, path, "inject", inject, error, JSONC_OPTIONAL); |
| 125 | |
| 126 | if(match) flags |= RW_MATCH_PCRE2; |
| 127 | else if(not_empty) flags |= RW_MATCH_NON_EMPTY; |
| 128 | if(!stop) flags |= RW_DONT_STOP; |
| 129 | if(inject) flags |= RW_INJECT; |
| 130 | |
| 131 | if(key && value) { |
| 132 | if(!log_job_rewrite_add(jb, key, flags, match ? match : not_empty, value)) { |
| 133 | buffer_sprintf(error, "failed to add rewrite for '%s.rewrite'", path); |
| 134 | return false; |
| 135 | } |
| 136 | } |
| 137 | }); |
| 138 | }); |
| 139 | |
| 140 | // Parse unmatched section (optional) |
| 141 | JSONC_PARSE_SUBOBJECT(jobj, path, "unmatched", error, JSONC_STRICT, { |
| 142 | CLEAN_CHAR_P *key = NULL; |
| 143 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "key", key, error, JSONC_OPTIONAL); |
| 144 | if(key) { |
| 145 | hashed_key_set(&jb->unmatched.key, key, strlen(key)); |
| 146 | } |
| 147 | |
| 148 | // Parse unmatched injections |
| 149 | JSONC_PARSE_ARRAY(jobj, path, "inject", error, JSONC_STRICT, { |
| 150 | size_t i; |
| 151 | JSONC_PARSE_ARRAY_ITEM_OBJECT(jobj, path, i, JSONC_REQUIRED, { |
| 152 | CLEAN_CHAR_P *inj_key = NULL; |
| 153 | CLEAN_CHAR_P *inj_value = NULL; |
| 154 | JSONC_PARSE_TXT2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "key", inj_key, error, JSONC_REQUIRED); |
| 155 | JSONC_PARSE_SCALAR2STRDUPZ_OR_ERROR_AND_RETURN(jobj, path, "value", inj_value, error, JSONC_REQUIRED); |
| 156 | if(inj_key && inj_value) { |
| 157 | if(!log_job_injection_add(jb, inj_key, strlen(inj_key), inj_value, strlen(inj_value), true)) { |
| 158 | buffer_sprintf(error, "failed to add unmatched injection for '%s.unmatched.inject'", path); |
| 159 | return false; |
| 160 | } |
| 161 | } |
| 162 | }); |
| 163 | }); |
| 164 | }); |
| 165 | |
| 166 | return true; |
| 167 | } |
| 168 | |
| 169 | bool yaml_parse_file(const char *config_file_path, LOG_JOB *jb) { |
| 170 | if(!config_file_path || !*config_file_path) { |
| 171 | l2j_log("yaml configuration filename cannot be empty."); |
| 172 | return false; |
| 173 | } |
| 174 | |
| 175 | BUFFER *error = buffer_create(0, NULL); |
| 176 | |
| 177 | // Parse YAML to JSON-C using libnetdata's YAML parser with all values as strings |
| 178 | struct json_object *json = yaml_parse_filename(config_file_path, error, YAML2JSON_ALL_VALUES_AS_STRINGS); |
| 179 | if (!json) { |
| 180 | l2j_log("Error parsing YAML file %s: %s", config_file_path, buffer_tostring(error)); |
| 181 | buffer_free(error); |
| 182 | return false; |
| 183 | } |
| 184 | |
| 185 | // Parse JSON-C to LOG_JOB structure |
| 186 | buffer_flush(error); |
| 187 | bool success = log2journal_config_from_json(json, jb, error); |
| 188 | |
| 189 | if(!success) { |
| 190 | l2j_log("Error parsing configuration: %s", buffer_tostring(error)); |
| 191 | } |
| 192 | |
| 193 | json_object_put(json); |
| 194 | buffer_free(error); |
| 195 | |
| 196 | return success; |
| 197 | } |
| 198 | |
| 199 | bool yaml_parse_config(const char *config_name, LOG_JOB *jb) { |
| 200 | char filename[FILENAME_MAX + 1]; |
| 201 | |
| 202 | snprintf(filename, sizeof(filename), "%s/%s.yaml", LOG2JOURNAL_CONFIG_PATH, config_name); |
| 203 | return yaml_parse_file(filename, jb); |
| 204 | } |
| 205 | |
| 206 | #endif // HAVE_LIBYAML |
| 207 | |
| 208 | // ---------------------------------------------------------------------------- |
| 209 | // printing yaml |
| 210 | |
| 211 | static void yaml_print_multiline_value(const char *s, size_t depth) { |
| 212 | if (!s) |
| 213 | s = ""; |
| 214 | |
| 215 | do { |
| 216 | const char* next = strchr(s, '\n'); |
| 217 | if(next) next++; |
| 218 | |
| 219 | size_t len = next ? (size_t)(next - s) : strlen(s); |
| 220 | char *buf = mallocz(len + 1); |
| 221 | copy_to_buffer(buf, len + 1, s, len); |
| 222 | |
| 223 | fprintf(stderr, "%.*s%s%s", |
| 224 | (int)(depth * 2), " ", |
| 225 | buf, next ? "" : "\n"); |
| 226 | |
| 227 | freez(buf); |
| 228 | s = next; |
| 229 | } while(s && *s); |
| 230 | } |
| 231 | |
| 232 | static bool needs_quotes_in_yaml(const char *str) { |
| 233 | // Lookup table for special YAML characters |
| 234 | static bool special_chars[256] = { false }; |
| 235 | static bool table_initialized = false; |
| 236 | |
| 237 | if (!table_initialized) { |
| 238 | // Initialize the lookup table |
| 239 | const char *special_chars_str = ":{}[],&*!|>'\"%@`^"; |
| 240 | for (const char *c = special_chars_str; *c; ++c) { |
| 241 | special_chars[(unsigned char)*c] = true; |
| 242 | } |
| 243 | table_initialized = true; |
| 244 | } |
| 245 | |
| 246 | while (*str) { |
| 247 | if (special_chars[(unsigned char)*str]) { |
| 248 | return true; |
| 249 | } |
| 250 | str++; |
| 251 | } |
| 252 | return false; |
| 253 | } |
| 254 | |
| 255 | static void yaml_print_node(const char *key, const char *value, size_t depth, bool dash) { |
| 256 | if(depth > 10) depth = 10; |
| 257 | const char *quote = "'"; |
| 258 | |
| 259 | const char *second_line = NULL; |
| 260 | if(value && strchr(value, '\n')) { |
| 261 | second_line = value; |
| 262 | value = "|"; |
| 263 | quote = ""; |
| 264 | } |
| 265 | else if(!value || !needs_quotes_in_yaml(value)) |
| 266 | quote = ""; |
| 267 | |
| 268 | fprintf(stderr, "%.*s%s%s%s%s%s%s\n", |
| 269 | (int)(depth * 2), " ", dash ? "- ": "", |
| 270 | key ? key : "", key ? ": " : "", |
| 271 | quote, value ? value : "", quote); |
| 272 | |
| 273 | if(second_line) { |
| 274 | yaml_print_multiline_value(second_line, depth + 1); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | void log_job_configuration_to_yaml(LOG_JOB *jb) { |
| 279 | if(jb->pattern) |
| 280 | yaml_print_node("pattern", jb->pattern, 0, false); |
| 281 | |
| 282 | if(jb->prefix) { |
| 283 | fprintf(stderr, "\n"); |
| 284 | yaml_print_node("prefix", jb->prefix, 0, false); |
| 285 | } |
| 286 | |
| 287 | if(jb->filename.key.key) { |
| 288 | fprintf(stderr, "\n"); |
| 289 | yaml_print_node("filename", NULL, 0, false); |
| 290 | yaml_print_node("key", jb->filename.key.key, 1, false); |
| 291 | } |
| 292 | |
| 293 | if(jb->filter.include.pattern || jb->filter.exclude.pattern) { |
| 294 | fprintf(stderr, "\n"); |
| 295 | yaml_print_node("filter", NULL, 0, false); |
| 296 | |
| 297 | if(jb->filter.include.pattern) |
| 298 | yaml_print_node("include", jb->filter.include.pattern, 1, false); |
| 299 | |
| 300 | if(jb->filter.exclude.pattern) |
| 301 | yaml_print_node("exclude", jb->filter.exclude.pattern, 1, false); |
| 302 | } |
| 303 | |
| 304 | if(jb->renames.used) { |
| 305 | fprintf(stderr, "\n"); |
| 306 | yaml_print_node("rename", NULL, 0, false); |
| 307 | |
| 308 | for(size_t i = 0; i < jb->renames.used ;i++) { |
| 309 | yaml_print_node("new_key", jb->renames.array[i].new_key.key, 1, true); |
| 310 | yaml_print_node("old_key", jb->renames.array[i].old_key.key, 2, false); |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | if(jb->injections.used) { |
| 315 | fprintf(stderr, "\n"); |
| 316 | yaml_print_node("inject", NULL, 0, false); |
| 317 | |
| 318 | for (size_t i = 0; i < jb->injections.used; i++) { |
| 319 | yaml_print_node("key", jb->injections.keys[i].key.key, 1, true); |
| 320 | yaml_print_node("value", jb->injections.keys[i].value.pattern, 2, false); |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | if(jb->rewrites.used) { |
| 325 | fprintf(stderr, "\n"); |
| 326 | yaml_print_node("rewrite", NULL, 0, false); |
| 327 | |
| 328 | for(size_t i = 0; i < jb->rewrites.used ;i++) { |
| 329 | REWRITE *rw = &jb->rewrites.array[i]; |
| 330 | |
| 331 | yaml_print_node("key", rw->key.key, 1, true); |
| 332 | |
| 333 | if(rw->flags & RW_MATCH_PCRE2) |
| 334 | yaml_print_node("match", rw->match_pcre2.pattern, 2, false); |
| 335 | |
| 336 | else if(rw->flags & RW_MATCH_NON_EMPTY) |
| 337 | yaml_print_node("not_empty", rw->match_non_empty.pattern, 2, false); |
| 338 | |
| 339 | yaml_print_node("value", rw->value.pattern, 2, false); |
| 340 | |
| 341 | if(rw->flags & RW_INJECT) |
| 342 | yaml_print_node("inject", "yes", 2, false); |
| 343 | |
| 344 | if(rw->flags & RW_DONT_STOP) |
| 345 | yaml_print_node("stop", "no", 2, false); |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | if(jb->unmatched.key.key || jb->unmatched.injections.used) { |
| 350 | fprintf(stderr, "\n"); |
| 351 | yaml_print_node("unmatched", NULL, 0, false); |
| 352 | |
| 353 | if(jb->unmatched.key.key) |
| 354 | yaml_print_node("key", jb->unmatched.key.key, 1, false); |
| 355 | |
| 356 | if(jb->unmatched.injections.used) { |
| 357 | fprintf(stderr, "\n"); |
| 358 | yaml_print_node("inject", NULL, 1, false); |
| 359 | |
| 360 | for (size_t i = 0; i < jb->unmatched.injections.used; i++) { |
| 361 | yaml_print_node("key", jb->unmatched.injections.keys[i].key.key, 2, true); |
| 362 | yaml_print_node("value", jb->unmatched.injections.keys[i].value.pattern, 3, false); |
| 363 | } |
| 364 | } |
| 365 | } |
| 366 | } |