diff --git a/scripts/journal-tail.sh b/scripts/journal-tail.sh index 8f857f1..6ca1782 100755 --- a/scripts/journal-tail.sh +++ b/scripts/journal-tail.sh @@ -1,90 +1,115 @@ #!/usr/bin/env bash -# journal-tail.sh — journald-Logs inkrementell als Influx Line Protocol liefern. +# journal-tail.sh — journald-Logs inkrementell als Influx Line Protocol. # # Warum dieses Script: # * inputs.journal existiert in den offiziellen Telegraf-Builds NICHT -# (Docker-Image + InfluxData-Debian-Paket 1.40 geprüft). -# * Auf PVE gibt es KEIN rsyslog — /var/log/syslog existiert dort nicht. +# (Docker-Image UND InfluxData-Debian-Paket 1.40 geprueft; das Plugin +# braucht einen Build mit systemd-Headern). +# * Auf PVE gibt es KEIN rsyslog -> /var/log/syslog existiert dort nicht. # * Der JSON-Parser von inputs.exec liest nur EIN Objekt, keine NDJSON-Zeilen. -# * Das fertige Line Protocol ist dasselbe Muster wie gpu-rocm.sh (bewährt). +# * Line Protocol direkt ist dasselbe bewaehrte Muster wie gpu-rocm.sh. # -# Zustand: der Cursor steckt in jeder journalctl-JSON-Zeile ("__CURSOR"). -# Er wird in $STATE_DIR/cursor gemerkt -> keine Duplikate, kein Verlust. +# PERFORMANCE: das Parsen laeuft in EINEM awk-Prozess. Eine Vorgaengerversion +# rief pro Feld ein sed auf (~2500 Prozessstarts bei 500 Zeilen) und riss +# dadurch Telegrafs exec-Timeout. Keine externen Abhaengigkeiten (kein jq). +# +# ZUSTAND: der Cursor steckt in jeder journalctl-JSON-Zeile ("__CURSOR"). +# Er wird am Ende des Laufs in $STATE_DIR/cursor geschrieben -> keine +# Duplikate, kein Verlust bei Neustart oder Journal-Rotation. # # Telegraf: # [[inputs.exec]] # interval = "30s" -# timeout = "25s" +# timeout = "28s" # data_format = "influx" # commands = ["/etc/telegraf/journal-tail.sh"] +# +# Feintuning via /etc/default/telegraf: +# JOURNAL_PRIORITY=warning warning|err|info|debug +# JOURNAL_MAX_LINES=500 Obergrenze pro Lauf +# JOURNAL_BUDGET=15 Sekunden, die journalctl hoechstens lesen darf set -u -PRIORITY="${JOURNAL_PRIORITY:-warning}" # warning|err|info|debug -# Hartes Zeitbudget: Telegraf bricht den exec sonst ab (timeout 25s bei -# interval 30s). Wir lesen nie laenger als BUDGET Sekunden pro Aufruf — der -# Cursor macht den Rest beim naechsten Lauf. +PRIORITY="${JOURNAL_PRIORITY:-warning}" +MAX_LINES="${JOURNAL_MAX_LINES:-500}" BUDGET="${JOURNAL_BUDGET:-15}" -SINCE="${JOURNAL_SINCE:-}" # optional, nur beim Erstanlauf -MAX_LINES="${JOURNAL_MAX_LINES:-500}" # Schutz vor Flut pro Lauf +SINCE="${JOURNAL_SINCE:-}" STATE_DIR="${JOURNAL_STATE_DIR:-/var/lib/telegraf-journal}" CURSOR_FILE="$STATE_DIR/cursor" mkdir -p "$STATE_DIR" 2>/dev/null || true -args=(-o json --no-pager --no-hostname -p "$PRIORITY" -n "$MAX_LINES") +# Wurde noch nie gelesen? Dann nicht die komplette Historie ausschuetten. if [ -s "$CURSOR_FILE" ]; then - args+=(--after-cursor "$(cat "$CURSOR_FILE" 2>/dev/null)") + START_ARGS=(--after-cursor "$(cat "$CURSOR_FILE" 2>/dev/null)") elif [ -n "$SINCE" ]; then - args+=(--since "$SINCE") + START_ARGS=(--since "$SINCE") else - args+=(--since "-5min") + START_ARGS=(--since "-5min") fi -last_cursor="" +# --no-hostname: den Hostnamen liefert Telegraf als global_tag (node). Der +# journaleigene Name waere sonst ein zweites, abweichendes Feld. +timeout -k 2 "$BUDGET" journalctl -o json --no-pager --no-hostname \ + -p "$PRIORITY" -n "$MAX_LINES" "${START_ARGS[@]}" 2>/dev/null \ +| awk -v TMP="$CURSOR_FILE.tmp" ' + # JSON-String-Feld lesen — beruecksichtigt Backslash-Escapes. + function jget(line, key, pat, p, rest, out, i, n, c) { + pat = "\"" key "\":\"" + p = index(line, pat) + if (p == 0) return "" + rest = substr(line, p + length(pat)) + out = ""; n = length(rest) + for (i = 1; i <= n; i++) { + c = substr(rest, i, 1) + if (c == "\\") { out = out c; i++; if (i <= n) out = out substr(rest, i, 1); continue } + if (c == "\"") break + out = out c + } + return out + } + # Tag-Werte: Komma, Leerzeichen, Gleichheitszeichen escapen + function etag(s) { gsub(/[ ,=]/, "\\\\&", s); return s } + # String-Feld: Backslash und Anfuehrungszeichen escapen + function estr(s) { gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); return s } -# --no-pager + hartes Zeitlimit: journalctl kann bei viel Aufkommen laenger -# brauchen als Telegrafs timeout erlaubt. -timeout -k 2 "$BUDGET" journalctl "${args[@]}" 2>/dev/null | while IFS= read -r line; do - case "$line" in \{*) ;; *) continue ;; esac + function emit( cur, msg, unit, ident, prio, ts, lhost, ns) { + cur = jget($0, "__CURSOR") + if (cur != "") last = cur - # Felder aus dem JSON ziehen (jq-frei, damit kein Extra-Paket nötig ist) - get() { printf '%s' "$line" | sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" | head -1; } - getnum() { printf '%s' "$line" | sed -n "s/.*\"$1\":\([0-9]*\).*/\1/p" | head -1; } + msg = jget($0, "MESSAGE") + if (msg == "") return - cur="$(get '__CURSOR')" - [ -n "$cur" ] && last_cursor="$cur" + unit = jget($0, "_SYSTEMD_UNIT"); if (unit == "") unit = jget($0, "SYSLOG_IDENTIFIER") + ident = jget($0, "SYSLOG_IDENTIFIER") + prio = jget($0, "PRIORITY") + ts = jget($0, "__REALTIME_TIMESTAMP") + lhost = jget($0, "_HOSTNAME") - msg="$(get 'MESSAGE')" - [ -n "$msg" ] || continue + if (unit == "") unit = "unknown" + if (ident == "") ident = "unknown" + if (prio == "") prio = "0" + if (lhost == "") lhost = "unknown" - unit="$(get '_SYSTEMD_UNIT')"; [ -n "$unit" ] || unit="$(get 'SYSLOG_IDENTIFIER')" - ident="$(get 'SYSLOG_IDENTIFIER')" - prio="$(get 'PRIORITY')" - ts_us="$(getnum '__REALTIME_TIMESTAMP')" - host="$(get '_HOSTNAME')" + # __REALTIME_TIMESTAMP ist in Mikrosekunden -> Nanosekunden + ns = (ts ~ /^[0-9]+$/) ? ts "000" : sprintf("%.0f000000000", systime()) - # Line Protocol: Escaping für Tag-Werte und String-Felder - esc_tag() { printf '%s' "$1" | sed 's/[ ,=]/\\&/g'; } - esc_str() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; } + # Tag heisst jhost, NICHT host: Telegraf setzt host= als global_tag, + # sonst filtert das Dashboard-Log-Panel nach dem falschen Knoten. + printf "journal,unit=%s,ident=%s,priority=%s,jhost=%s message=\"%s\" %s\n", \ + etag(unit), etag(ident), etag(prio), etag(lhost), estr(msg), ns + count++ + } - # Nanosekunden: __REALTIME_TIMESTAMP ist in Mikrosekunden - if [ -n "$ts_us" ]; then - ns="${ts_us}000" - else - ns="$(date +%s)000000000" - fi + { if (substr($0, 1, 1) == "{") emit() } - # Tag "jhost" statt "host": Telegrafs global_tag host= hat Vorrang behalten, - # sonst filtert das Dashboard-Log-Panel nach dem falschen Knoten. - printf 'journal,unit=%s,ident=%s,priority=%s,jhost=%s message="%s" %s\n' \ - "$(esc_tag "${unit:-unknown}")" \ - "$(esc_tag "${ident:-unknown}")" \ - "$(esc_tag "${prio:-0}")" \ - "$(esc_tag "${host:-unknown}")" \ - "$(esc_str "$msg")" \ - "$ns" + END { + # Cursor einmal am Ende sichern — atomar ersetzt das aufrufende Shell-Skript. + if (last != "") { printf "%s", last > TMP; close(TMP) } + } +' - # Zustand nach jedem Datensatz sichern - [ -n "$cur" ] && { printf '%s' "$cur" > "$CURSOR_FILE.tmp" && mv -f "$CURSOR_FILE.tmp" "$CURSOR_FILE"; } -done +if [ -s "$CURSOR_FILE.tmp" ]; then + mv -f "$CURSOR_FILE.tmp" "$CURSOR_FILE" 2>/dev/null || true +fi