#!/usr/bin/env bash # journal-tail.sh — journald-Logs inkrementell als Influx Line Protocol. # # Warum dieses Script: # * inputs.journal existiert in den offiziellen Telegraf-Builds 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. # * Line Protocol direkt ist dasselbe bewaehrte Muster wie gpu-rocm.sh. # # 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 = "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}" MAX_LINES="${JOURNAL_MAX_LINES:-500}" BUDGET="${JOURNAL_BUDGET:-15}" 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 # Wurde noch nie gelesen? Dann nicht die komplette Historie ausschuetten. if [ -s "$CURSOR_FILE" ]; then START_ARGS=(--after-cursor "$(cat "$CURSOR_FILE" 2>/dev/null)") elif [ -n "$SINCE" ]; then START_ARGS=(--since "$SINCE") else START_ARGS=(--since "-5min") fi # --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 } function emit( cur, msg, unit, ident, prio, ts, lhost, ns) { cur = jget($0, "__CURSOR") if (cur != "") last = cur msg = jget($0, "MESSAGE") if (msg == "") return 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") if (unit == "") unit = "unknown" if (ident == "") ident = "unknown" if (prio == "") prio = "0" if (lhost == "") lhost = "unknown" # __REALTIME_TIMESTAMP ist in Mikrosekunden -> Nanosekunden ns = (ts ~ /^[0-9]+$/) ? ts "000" : sprintf("%.0f000000000", systime()) # 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++ } { if (substr($0, 1, 1) == "{") emit() } END { # Cursor einmal am Ende sichern — atomar ersetzt das aufrufende Shell-Skript. if (last != "") { printf "%s", last > TMP; close(TMP) } } ' if [ -s "$CURSOR_FILE.tmp" ]; then mv -f "$CURSOR_FILE.tmp" "$CURSOR_FILE" 2>/dev/null || true fi