| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "log2journal.h" |
| 4 | |
| 5 | // ---------------------------------------------------------------------------- |
| 6 | |
| 7 | const char journal_key_characters_map[256] = { |
| 8 | // control characters |
| 9 | [0] = '\0', [1] = '_', [2] = '_', [3] = '_', [4] = '_', [5] = '_', [6] = '_', [7] = '_', |
| 10 | [8] = '_', [9] = '_', [10] = '_', [11] = '_', [12] = '_', [13] = '_', [14] = '_', [15] = '_', |
| 11 | [16] = '_', [17] = '_', [18] = '_', [19] = '_', [20] = '_', [21] = '_', [22] = '_', [23] = '_', |
| 12 | [24] = '_', [25] = '_', [26] = '_', [27] = '_', [28] = '_', [29] = '_', [30] = '_', [31] = '_', |
| 13 | |
| 14 | // symbols |
| 15 | [' '] = '_', ['!'] = '_', ['"'] = '_', ['#'] = '_', ['$'] = '_', ['%'] = '_', ['&'] = '_', ['\''] = '_', |
| 16 | ['('] = '_', [')'] = '_', ['*'] = '_', ['+'] = '_', [','] = '_', ['-'] = '_', ['.'] = '_', ['/'] = '_', |
| 17 | |
| 18 | // numbers |
| 19 | ['0'] = '0', ['1'] = '1', ['2'] = '2', ['3'] = '3', ['4'] = '4', ['5'] = '5', ['6'] = '6', ['7'] = '7', |
| 20 | ['8'] = '8', ['9'] = '9', |
| 21 | |
| 22 | // symbols |
| 23 | [':'] = '_', [';'] = '_', ['<'] = '_', ['='] = '_', ['>'] = '_', ['?'] = '_', ['@'] = '_', |
| 24 | |
| 25 | // capitals |
| 26 | ['A'] = 'A', ['B'] = 'B', ['C'] = 'C', ['D'] = 'D', ['E'] = 'E', ['F'] = 'F', ['G'] = 'G', ['H'] = 'H', |
| 27 | ['I'] = 'I', ['J'] = 'J', ['K'] = 'K', ['L'] = 'L', ['M'] = 'M', ['N'] = 'N', ['O'] = 'O', ['P'] = 'P', |
| 28 | ['Q'] = 'Q', ['R'] = 'R', ['S'] = 'S', ['T'] = 'T', ['U'] = 'U', ['V'] = 'V', ['W'] = 'W', ['X'] = 'X', |
| 29 | ['Y'] = 'Y', ['Z'] = 'Z', |
| 30 | |
| 31 | // symbols |
| 32 | ['['] = '_', ['\\'] = '_', [']'] = '_', ['^'] = '_', ['_'] = '_', ['`'] = '_', |
| 33 | |
| 34 | // lower to upper |
| 35 | ['a'] = 'A', ['b'] = 'B', ['c'] = 'C', ['d'] = 'D', ['e'] = 'E', ['f'] = 'F', ['g'] = 'G', ['h'] = 'H', |
| 36 | ['i'] = 'I', ['j'] = 'J', ['k'] = 'K', ['l'] = 'L', ['m'] = 'M', ['n'] = 'N', ['o'] = 'O', ['p'] = 'P', |
| 37 | ['q'] = 'Q', ['r'] = 'R', ['s'] = 'S', ['t'] = 'T', ['u'] = 'U', ['v'] = 'V', ['w'] = 'W', ['x'] = 'X', |
| 38 | ['y'] = 'Y', ['z'] = 'Z', |
| 39 | |
| 40 | // symbols |
| 41 | ['{'] = '_', ['|'] = '_', ['}'] = '_', ['~'] = '_', [127] = '_', // Delete (DEL) |
| 42 | |
| 43 | // Extended ASCII characters (128-255) set to underscore |
| 44 | [128] = '_', [129] = '_', [130] = '_', [131] = '_', [132] = '_', [133] = '_', [134] = '_', [135] = '_', |
| 45 | [136] = '_', [137] = '_', [138] = '_', [139] = '_', [140] = '_', [141] = '_', [142] = '_', [143] = '_', |
| 46 | [144] = '_', [145] = '_', [146] = '_', [147] = '_', [148] = '_', [149] = '_', [150] = '_', [151] = '_', |
| 47 | [152] = '_', [153] = '_', [154] = '_', [155] = '_', [156] = '_', [157] = '_', [158] = '_', [159] = '_', |
| 48 | [160] = '_', [161] = '_', [162] = '_', [163] = '_', [164] = '_', [165] = '_', [166] = '_', [167] = '_', |
| 49 | [168] = '_', [169] = '_', [170] = '_', [171] = '_', [172] = '_', [173] = '_', [174] = '_', [175] = '_', |
| 50 | [176] = '_', [177] = '_', [178] = '_', [179] = '_', [180] = '_', [181] = '_', [182] = '_', [183] = '_', |
| 51 | [184] = '_', [185] = '_', [186] = '_', [187] = '_', [188] = '_', [189] = '_', [190] = '_', [191] = '_', |
| 52 | [192] = '_', [193] = '_', [194] = '_', [195] = '_', [196] = '_', [197] = '_', [198] = '_', [199] = '_', |
| 53 | [200] = '_', [201] = '_', [202] = '_', [203] = '_', [204] = '_', [205] = '_', [206] = '_', [207] = '_', |
| 54 | [208] = '_', [209] = '_', [210] = '_', [211] = '_', [212] = '_', [213] = '_', [214] = '_', [215] = '_', |
| 55 | [216] = '_', [217] = '_', [218] = '_', [219] = '_', [220] = '_', [221] = '_', [222] = '_', [223] = '_', |
| 56 | [224] = '_', [225] = '_', [226] = '_', [227] = '_', [228] = '_', [229] = '_', [230] = '_', [231] = '_', |
| 57 | [232] = '_', [233] = '_', [234] = '_', [235] = '_', [236] = '_', [237] = '_', [238] = '_', [239] = '_', |
| 58 | [240] = '_', [241] = '_', [242] = '_', [243] = '_', [244] = '_', [245] = '_', [246] = '_', [247] = '_', |
| 59 | [248] = '_', [249] = '_', [250] = '_', [251] = '_', [252] = '_', [253] = '_', [254] = '_', [255] = '_', |
| 60 | }; |
| 61 | |
| 62 | // ---------------------------------------------------------------------------- |
| 63 | |
| 64 | static inline HASHED_KEY *get_key_from_hashtable(LOG_JOB *jb, HASHED_KEY *k) { |
| 65 | if(k->flags & HK_HASHTABLE_ALLOCATED) |
| 66 | return k; |
| 67 | |
| 68 | if(!k->hashtable_ptr) { |
| 69 | HASHED_KEY *ht_key; |
| 70 | SIMPLE_HASHTABLE_SLOT_KEY *slot = simple_hashtable_get_slot_KEY(&jb->hashtable, k->hash, NULL, true); |
| 71 | if((ht_key = SIMPLE_HASHTABLE_SLOT_DATA(slot))) { |
| 72 | if(!(ht_key->flags & HK_COLLISION_CHECKED)) { |
| 73 | ht_key->flags |= HK_COLLISION_CHECKED; |
| 74 | |
| 75 | if(strcmp(ht_key->key, k->key) != 0) |
| 76 | l2j_log( |
| 77 | "Hashtable collision detected on key '%s' (hash %lx) and '%s' (hash %lx). " |
| 78 | "Please file a bug report.", |
| 79 | ht_key->key, |
| 80 | (unsigned long)ht_key->hash, |
| 81 | k->key, |
| 82 | (unsigned long)k->hash); |
| 83 | } |
| 84 | } |
| 85 | else { |
| 86 | ht_key = callocz(1, sizeof(HASHED_KEY)); |
| 87 | ht_key->key = strdupz(k->key); |
| 88 | ht_key->len = k->len; |
| 89 | ht_key->hash = k->hash; |
| 90 | ht_key->flags = HK_HASHTABLE_ALLOCATED; |
| 91 | |
| 92 | simple_hashtable_set_slot_KEY(&jb->hashtable, slot, ht_key->hash, ht_key); |
| 93 | } |
| 94 | |
| 95 | k->hashtable_ptr = ht_key; |
| 96 | } |
| 97 | |
| 98 | return k->hashtable_ptr; |
| 99 | } |
| 100 | |
| 101 | static inline HASHED_KEY *get_key_from_hashtable_with_char_ptr(LOG_JOB *jb, const char *key) { |
| 102 | HASHED_KEY find = { |
| 103 | .flags = HK_NONE, |
| 104 | .key = key, |
| 105 | .len = strlen(key), |
| 106 | }; |
| 107 | find.hash = XXH3_64bits(key, find.len); |
| 108 | |
| 109 | return get_key_from_hashtable(jb, &find); |
| 110 | } |
| 111 | |
| 112 | // ---------------------------------------------------------------------------- |
| 113 | |
| 114 | static inline void validate_key(LOG_JOB *jb __maybe_unused, HASHED_KEY *k) { |
| 115 | if(k->len > JOURNAL_MAX_KEY_LEN) |
| 116 | l2j_log( |
| 117 | "WARNING: key '%s' has length %zu, which is more than %zu, the max systemd-journal allows", |
| 118 | k->key, |
| 119 | (size_t)k->len, |
| 120 | (size_t)JOURNAL_MAX_KEY_LEN); |
| 121 | |
| 122 | for(size_t i = 0; i < k->len ;i++) { |
| 123 | char c = k->key[i]; |
| 124 | |
| 125 | if((c < 'A' || c > 'Z') && !isdigit(c) && c != '_') { |
| 126 | l2j_log("WARNING: key '%s' contains characters that are not allowed by systemd-journal.", k->key); |
| 127 | break; |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | if(isdigit(k->key[0])) |
| 132 | l2j_log("WARNING: key '%s' starts with a digit and may not be accepted by systemd-journal.", k->key); |
| 133 | |
| 134 | if(k->key[0] == '_') |
| 135 | l2j_log( |
| 136 | "WARNING: key '%s' starts with an underscore, which makes it a systemd-journal trusted field. " |
| 137 | "Such fields are accepted by systemd-journal-remote, but not by systemd-journald.", |
| 138 | k->key); |
| 139 | } |
| 140 | |
| 141 | // ---------------------------------------------------------------------------- |
| 142 | |
| 143 | static inline size_t replace_evaluate_to_buffer(LOG_JOB *jb, HASHED_KEY *k __maybe_unused, REPLACE_PATTERN *rp, char *dst, size_t dst_size) { |
| 144 | size_t remaining = dst_size; |
| 145 | char *copy_to = dst; |
| 146 | |
| 147 | for(REPLACE_NODE *node = rp->nodes; node != NULL && remaining > 1; node = node->next) { |
| 148 | if(node->is_variable) { |
| 149 | if(hashed_keys_match(&node->name, &jb->line.key)) { |
| 150 | size_t copied = copy_to_buffer(copy_to, remaining, jb->line.trimmed, jb->line.trimmed_len); |
| 151 | copy_to += copied; |
| 152 | remaining -= copied; |
| 153 | } |
| 154 | else { |
| 155 | HASHED_KEY *ktmp = get_key_from_hashtable_with_char_ptr(jb, node->name.key); |
| 156 | if(ktmp->value.len) { |
| 157 | size_t copied = copy_to_buffer(copy_to, remaining, ktmp->value.txt, ktmp->value.len); |
| 158 | copy_to += copied; |
| 159 | remaining -= copied; |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | else { |
| 164 | size_t copied = copy_to_buffer(copy_to, remaining, node->name.key, node->name.len); |
| 165 | copy_to += copied; |
| 166 | remaining -= copied; |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | return copy_to - dst; |
| 171 | } |
| 172 | |
| 173 | static inline void replace_evaluate(LOG_JOB *jb, HASHED_KEY *k, REPLACE_PATTERN *rp) { |
| 174 | HASHED_KEY *ht_key = get_key_from_hashtable(jb, k); |
| 175 | |
| 176 | // set it to empty value |
| 177 | k->value.len = 0; |
| 178 | |
| 179 | for(REPLACE_NODE *node = rp->nodes; node != NULL; node = node->next) { |
| 180 | if(node->is_variable) { |
| 181 | if(hashed_keys_match(&node->name, &jb->line.key)) |
| 182 | txt_l2j_append(&ht_key->value, jb->line.trimmed, jb->line.trimmed_len); |
| 183 | |
| 184 | else { |
| 185 | HASHED_KEY *ktmp = get_key_from_hashtable_with_char_ptr(jb, node->name.key); |
| 186 | if(ktmp->value.len) |
| 187 | txt_l2j_append(&ht_key->value, ktmp->value.txt, ktmp->value.len); |
| 188 | } |
| 189 | } |
| 190 | else |
| 191 | txt_l2j_append(&ht_key->value, node->name.key, node->name.len); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | static inline void replace_evaluate_from_pcre2(LOG_JOB *jb, HASHED_KEY *k, REPLACE_PATTERN *rp, SEARCH_PATTERN *sp) { |
| 196 | assert(k->flags & HK_HASHTABLE_ALLOCATED); |
| 197 | |
| 198 | // set the temporary TEXT to zero length |
| 199 | jb->rewrites.tmp.len = 0; |
| 200 | |
| 201 | PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(sp->match_data); |
| 202 | |
| 203 | // Iterate through the linked list of replacement nodes |
| 204 | for(REPLACE_NODE *node = rp->nodes; node != NULL; node = node->next) { |
| 205 | if(node->is_variable) { |
| 206 | int group_number = pcre2_substring_number_from_name( |
| 207 | sp->re, (PCRE2_SPTR) node->name.key); |
| 208 | |
| 209 | if(group_number >= 0) { |
| 210 | PCRE2_SIZE start_offset = ovector[2 * group_number]; |
| 211 | PCRE2_SIZE end_offset = ovector[2 * group_number + 1]; |
| 212 | PCRE2_SIZE length = end_offset - start_offset; |
| 213 | |
| 214 | txt_l2j_append(&jb->rewrites.tmp, k->value.txt + start_offset, length); |
| 215 | } |
| 216 | else { |
| 217 | if(hashed_keys_match(&node->name, &jb->line.key)) |
| 218 | txt_l2j_append(&jb->rewrites.tmp, jb->line.trimmed, jb->line.trimmed_len); |
| 219 | |
| 220 | else { |
| 221 | HASHED_KEY *ktmp = get_key_from_hashtable_with_char_ptr(jb, node->name.key); |
| 222 | if(ktmp->value.len) |
| 223 | txt_l2j_append(&jb->rewrites.tmp, ktmp->value.txt, ktmp->value.len); |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | else { |
| 228 | txt_l2j_append(&jb->rewrites.tmp, node->name.key, node->name.len); |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | // swap the values of the temporary TEXT and the key value |
| 233 | TXT_L2J tmp = k->value; |
| 234 | k->value = jb->rewrites.tmp; |
| 235 | jb->rewrites.tmp = tmp; |
| 236 | } |
| 237 | |
| 238 | static inline bool rewrite_conditions_satisfied(LOG_JOB *jb, HASHED_KEY *k, REWRITE *rw) { |
| 239 | assert(k->flags & HK_HASHTABLE_ALLOCATED); |
| 240 | |
| 241 | if(rw->flags & RW_MATCH_PCRE2) { |
| 242 | return search_pattern_matches(&rw->match_pcre2, k->value.txt, k->value.len); |
| 243 | } |
| 244 | else if(rw->flags & RW_MATCH_NON_EMPTY) { |
| 245 | char buffer[2]; // we don't need a big buffer - we just check if anything is written |
| 246 | if(replace_evaluate_to_buffer(jb, k, &rw->match_non_empty, buffer, sizeof(buffer))) |
| 247 | // it copied something |
| 248 | return true; |
| 249 | else |
| 250 | // it copied nothing |
| 251 | return false; |
| 252 | } |
| 253 | else |
| 254 | // no conditions |
| 255 | return true; |
| 256 | } |
| 257 | |
| 258 | // ---------------------------------------------------------------------------- |
| 259 | |
| 260 | static inline HASHED_KEY *rename_key(LOG_JOB *jb, HASHED_KEY *k) { |
| 261 | if(!(k->flags & HK_RENAMES_CHECKED) || k->flags & HK_HAS_RENAMES) { |
| 262 | k->flags |= HK_RENAMES_CHECKED; |
| 263 | |
| 264 | for(size_t i = 0; i < jb->renames.used; i++) { |
| 265 | RENAME *rn = &jb->renames.array[i]; |
| 266 | |
| 267 | if(hashed_keys_match(&rn->old_key, k)) { |
| 268 | k->flags |= HK_HAS_RENAMES; |
| 269 | |
| 270 | return get_key_from_hashtable(jb, &rn->new_key); |
| 271 | } |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | return k; |
| 276 | } |
| 277 | |
| 278 | // ---------------------------------------------------------------------------- |
| 279 | |
| 280 | static inline void send_key_value_constant(LOG_JOB *jb __maybe_unused, HASHED_KEY *key, const char *value, size_t len) { |
| 281 | HASHED_KEY *ht_key = get_key_from_hashtable(jb, key); |
| 282 | |
| 283 | txt_l2j_set(&ht_key->value, value, len); |
| 284 | ht_key->flags |= HK_VALUE_FROM_LOG; |
| 285 | |
| 286 | // fprintf(stderr, "SET %s=%.*s\n", ht_key->key, (int)ht_key->value.len, ht_key->value.txt); |
| 287 | } |
| 288 | |
| 289 | static inline void send_key_value_error(LOG_JOB *jb, HASHED_KEY *key, const char *format, ...) PRINTFLIKE(3, 4); |
| 290 | static inline void send_key_value_error(LOG_JOB *jb, HASHED_KEY *key, const char *format, ...) { |
| 291 | HASHED_KEY *ht_key = get_key_from_hashtable(jb, key); |
| 292 | |
| 293 | printf("%s=", ht_key->key); |
| 294 | va_list args; |
| 295 | va_start(args, format); |
| 296 | vprintf(format, args); |
| 297 | va_end(args); |
| 298 | printf("\n"); |
| 299 | } |
| 300 | |
| 301 | inline void log_job_send_extracted_key_value(LOG_JOB *jb, const char *key, const char *value, size_t len) { |
| 302 | HASHED_KEY *ht_key = get_key_from_hashtable_with_char_ptr(jb, key); |
| 303 | HASHED_KEY *nk = rename_key(jb, ht_key); |
| 304 | txt_l2j_set(&nk->value, value, len); |
| 305 | ht_key->flags |= HK_VALUE_FROM_LOG; |
| 306 | |
| 307 | // fprintf(stderr, "SET %s=%.*s\n", ht_key->key, (int)ht_key->value.len, ht_key->value.txt); |
| 308 | } |
| 309 | |
| 310 | static inline void log_job_process_rewrites(LOG_JOB *jb) { |
| 311 | for(size_t i = 0; i < jb->rewrites.used ;i++) { |
| 312 | REWRITE *rw = &jb->rewrites.array[i]; |
| 313 | |
| 314 | HASHED_KEY *k = get_key_from_hashtable(jb, &rw->key); |
| 315 | |
| 316 | if(!(rw->flags & RW_INJECT) && !(k->flags & HK_VALUE_FROM_LOG) && !k->value.len) |
| 317 | continue; |
| 318 | |
| 319 | if(!(k->flags & HK_VALUE_REWRITTEN) && rewrite_conditions_satisfied(jb, k, rw)) { |
| 320 | if(rw->flags & RW_MATCH_PCRE2) |
| 321 | replace_evaluate_from_pcre2(jb, k, &rw->value, &rw->match_pcre2); |
| 322 | else |
| 323 | replace_evaluate(jb, k, &rw->value); |
| 324 | |
| 325 | if(!(rw->flags & RW_DONT_STOP)) |
| 326 | k->flags |= HK_VALUE_REWRITTEN; |
| 327 | |
| 328 | // fprintf(stderr, "REWRITE %s=%.*s\n", k->key, (int)k->value.len, k->value.txt); |
| 329 | } |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | static inline void send_all_fields(LOG_JOB *jb) { |
| 334 | SIMPLE_HASHTABLE_SORTED_FOREACH_READ_ONLY(&jb->hashtable, kptr, HASHED_KEY, _KEY) { |
| 335 | HASHED_KEY *k = SIMPLE_HASHTABLE_SORTED_FOREACH_READ_ONLY_VALUE(kptr); |
| 336 | |
| 337 | if(k->value.len) { |
| 338 | // the key exists and has some value |
| 339 | |
| 340 | if(!(k->flags & HK_FILTERED)) { |
| 341 | k->flags |= HK_FILTERED; |
| 342 | |
| 343 | bool included = jb->filter.include.re ? search_pattern_matches(&jb->filter.include, k->key, k->len) : true; |
| 344 | bool excluded = jb->filter.exclude.re ? search_pattern_matches(&jb->filter.exclude, k->key, k->len) : false; |
| 345 | |
| 346 | if(included && !excluded) |
| 347 | k->flags |= HK_FILTERED_INCLUDED; |
| 348 | else |
| 349 | k->flags &= ~HK_FILTERED_INCLUDED; |
| 350 | |
| 351 | // log some error if the key does not comply to journal standards |
| 352 | validate_key(jb, k); |
| 353 | } |
| 354 | |
| 355 | if(k->flags & HK_FILTERED_INCLUDED) |
| 356 | printf("%s=%.*s\n", k->key, (int)k->value.len, k->value.txt); |
| 357 | |
| 358 | // reset it for the next round |
| 359 | k->value.txt[0] = '\0'; |
| 360 | k->value.len = 0; |
| 361 | } |
| 362 | |
| 363 | k->flags &= ~(HK_VALUE_REWRITTEN | HK_VALUE_FROM_LOG); |
| 364 | } |
| 365 | } |
| 366 | |
| 367 | // ---------------------------------------------------------------------------- |
| 368 | // injection of constant fields |
| 369 | |
| 370 | static void select_which_injections_should_be_injected_on_unmatched(LOG_JOB *jb) { |
| 371 | // mark all injections to be added to unmatched logs |
| 372 | for(size_t i = 0; i < jb->injections.used ; i++) |
| 373 | jb->injections.keys[i].on_unmatched = true; |
| 374 | |
| 375 | if(jb->injections.used && jb->unmatched.injections.used) { |
| 376 | // we have both injections and injections on unmatched |
| 377 | |
| 378 | // we find all the injections that are also configured as injections on unmatched, |
| 379 | // and we disable them, so that the output will not have the same key twice |
| 380 | |
| 381 | for(size_t i = 0; i < jb->injections.used ;i++) { |
| 382 | for(size_t u = 0; u < jb->unmatched.injections.used ; u++) { |
| 383 | if(strcmp(jb->injections.keys[i].key.key, jb->unmatched.injections.keys[u].key.key) == 0) |
| 384 | jb->injections.keys[i].on_unmatched = false; |
| 385 | } |
| 386 | } |
| 387 | } |
| 388 | } |
| 389 | |
| 390 | |
| 391 | static inline void jb_finalize_injections(LOG_JOB *jb, bool line_is_matched) { |
| 392 | for (size_t j = 0; j < jb->injections.used; j++) { |
| 393 | if(!line_is_matched && !jb->injections.keys[j].on_unmatched) |
| 394 | continue; |
| 395 | |
| 396 | INJECTION *inj = &jb->injections.keys[j]; |
| 397 | |
| 398 | replace_evaluate(jb, &inj->key, &inj->value); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | // ---------------------------------------------------------------------------- |
| 403 | // filename injection |
| 404 | |
| 405 | static inline void jb_inject_filename(LOG_JOB *jb) { |
| 406 | if (jb->filename.key.key && jb->filename.current.len) |
| 407 | send_key_value_constant(jb, &jb->filename.key, jb->filename.current.txt, jb->filename.current.len); |
| 408 | } |
| 409 | |
| 410 | static inline bool jb_switched_filename(LOG_JOB *jb, const char *line, size_t len) { |
| 411 | // IMPORTANT: |
| 412 | // Return TRUE when the caller should skip this line (because it is ours). |
| 413 | // Unfortunately, we have to consume empty lines too. |
| 414 | |
| 415 | // IMPORTANT: |
| 416 | // filename may not be NULL terminated and have more data than the filename. |
| 417 | |
| 418 | if (!len) { |
| 419 | jb->filename.last_line_was_empty = true; |
| 420 | return true; |
| 421 | } |
| 422 | |
| 423 | // Check if it's a log file change line |
| 424 | if (jb->filename.last_line_was_empty && line[0] == '=' && strncmp(line, "==> ", 4) == 0) { |
| 425 | const char *start = line + 4; |
| 426 | const char *end = strstr(line, " <=="); |
| 427 | while (*start == ' ') start++; |
| 428 | if (*start != '\n' && *start != '\0' && end) { |
| 429 | txt_l2j_set(&jb->filename.current, start, end - start); |
| 430 | return true; |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | jb->filename.last_line_was_empty = false; |
| 435 | return false; |
| 436 | } |
| 437 | |
| 438 | static inline bool jb_send_unmatched_line(LOG_JOB *jb, const char *line) { |
| 439 | if (!jb->unmatched.key.key) |
| 440 | return false; |
| 441 | |
| 442 | // we are sending errors to systemd-journal |
| 443 | send_key_value_error(jb, &jb->unmatched.key, "Parsing error on: %s", line); |
| 444 | |
| 445 | for (size_t j = 0; j < jb->unmatched.injections.used; j++) { |
| 446 | INJECTION *inj = &jb->unmatched.injections.keys[j]; |
| 447 | |
| 448 | replace_evaluate(jb, &inj->key, &inj->value); |
| 449 | } |
| 450 | |
| 451 | return true; |
| 452 | } |
| 453 | |
| 454 | // ---------------------------------------------------------------------------- |
| 455 | // running a job |
| 456 | |
| 457 | static char *get_next_line(LOG_JOB *jb __maybe_unused, char *buffer, size_t size, size_t *line_length) { |
| 458 | if(!fgets(buffer, (int)size, stdin)) { |
| 459 | *line_length = 0; |
| 460 | return NULL; |
| 461 | } |
| 462 | |
| 463 | char *line = buffer; |
| 464 | size_t len = strlen(line); |
| 465 | |
| 466 | // remove trailing newlines and spaces |
| 467 | while(len > 1 && (line[len - 1] == '\n' || isspace(line[len - 1]))) |
| 468 | line[--len] = '\0'; |
| 469 | |
| 470 | // skip leading spaces |
| 471 | while(isspace(*line)) { |
| 472 | line++; |
| 473 | len--; |
| 474 | } |
| 475 | |
| 476 | *line_length = len; |
| 477 | return line; |
| 478 | } |
| 479 | |
| 480 | int log_job_run(LOG_JOB *jb) { |
| 481 | select_which_injections_should_be_injected_on_unmatched(jb); |
| 482 | |
| 483 | PCRE2_STATE *pcre2 = NULL; |
| 484 | LOG_JSON_STATE *json = NULL; |
| 485 | LOGFMT_STATE *logfmt = NULL; |
| 486 | |
| 487 | if(strcmp(jb->pattern, "json") == 0) { |
| 488 | json = json_parser_create(jb); |
| 489 | // never fails |
| 490 | } |
| 491 | else if(strcmp(jb->pattern, "logfmt") == 0) { |
| 492 | logfmt = logfmt_parser_create(jb); |
| 493 | // never fails |
| 494 | } |
| 495 | else if(strcmp(jb->pattern, "none") != 0) { |
| 496 | pcre2 = pcre2_parser_create(jb); |
| 497 | if(pcre2_has_error(pcre2)) { |
| 498 | l2j_log("%s", pcre2_parser_error(pcre2)); |
| 499 | pcre2_parser_destroy(pcre2); |
| 500 | return 1; |
| 501 | } |
| 502 | } |
| 503 | |
| 504 | jb->line.buffer = mallocz(MAX_LINE_LENGTH + 1); |
| 505 | jb->line.size = MAX_LINE_LENGTH + 1; |
| 506 | jb->line.trimmed_len = 0; |
| 507 | jb->line.trimmed = jb->line.buffer; |
| 508 | |
| 509 | while ((jb->line.trimmed = get_next_line(jb, (char *)jb->line.buffer, jb->line.size, &jb->line.trimmed_len))) { |
| 510 | const char *line = jb->line.trimmed; |
| 511 | size_t len = jb->line.trimmed_len; |
| 512 | |
| 513 | if(jb_switched_filename(jb, line, len)) |
| 514 | continue; |
| 515 | |
| 516 | bool line_is_matched = true; |
| 517 | |
| 518 | if(json) |
| 519 | line_is_matched = json_parse_document(json, line); |
| 520 | else if(logfmt) |
| 521 | line_is_matched = logfmt_parse_document(logfmt, line); |
| 522 | else if(pcre2) |
| 523 | line_is_matched = pcre2_parse_document(pcre2, line, len); |
| 524 | |
| 525 | if(!line_is_matched) { |
| 526 | if(json) |
| 527 | l2j_log("%s", json_parser_error(json)); |
| 528 | else if(logfmt) |
| 529 | l2j_log("%s", logfmt_parser_error(logfmt)); |
| 530 | else if(pcre2) |
| 531 | l2j_log("%s", pcre2_parser_error(pcre2)); |
| 532 | |
| 533 | if(!jb_send_unmatched_line(jb, line)) |
| 534 | // just logging to stderr, not sending unmatched lines |
| 535 | continue; |
| 536 | } |
| 537 | |
| 538 | jb_inject_filename(jb); |
| 539 | jb_finalize_injections(jb, line_is_matched); |
| 540 | |
| 541 | log_job_process_rewrites(jb); |
| 542 | send_all_fields(jb); |
| 543 | printf("\n"); |
| 544 | fflush(stdout); |
| 545 | } |
| 546 | |
| 547 | if(json) |
| 548 | json_parser_destroy(json); |
| 549 | |
| 550 | else if(logfmt) |
| 551 | logfmt_parser_destroy(logfmt); |
| 552 | |
| 553 | else if(pcre2) |
| 554 | pcre2_parser_destroy(pcre2); |
| 555 | |
| 556 | freez((void *)jb->line.buffer); |
| 557 | |
| 558 | return 0; |
| 559 | } |
| 560 | |
| 561 | // ---------------------------------------------------------------------------- |
| 562 | |
| 563 | int main(int argc, char *argv[]) { |
| 564 | LOG_JOB log_job; |
| 565 | |
| 566 | log_job_init(&log_job); |
| 567 | |
| 568 | if(!log_job_command_line_parse_parameters(&log_job, argc, argv)) |
| 569 | exit(1); |
| 570 | |
| 571 | if(log_job.show_config) |
| 572 | log_job_configuration_to_yaml(&log_job); |
| 573 | |
| 574 | int ret = log_job_run(&log_job); |
| 575 | |
| 576 | log_job_cleanup(&log_job); |
| 577 | return ret; |
| 578 | } |