perf(logs): journal-tail parst in einem awk-Durchlauf (11s -> 143ms, exec-Timeout behoben)

This commit is contained in:
mm committed 2026-09-13 22:32:07 +02:00
1 parent b49f9ccbfd
commit 8ca06bd6d6
1 file changed
+80 -55
+80 -55
View File
@@ -1,90 +1,115 @@
#!/usr/bin/env bash #!/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: # Warum dieses Script:
# * inputs.journal existiert in den offiziellen Telegraf-Builds NICHT # * inputs.journal existiert in den offiziellen Telegraf-Builds NICHT
# (Docker-Image + InfluxData-Debian-Paket 1.40 geprüft). # (Docker-Image UND InfluxData-Debian-Paket 1.40 geprueft; das Plugin
# * Auf PVE gibt es KEIN rsyslog — /var/log/syslog existiert dort nicht. # 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. # * 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"). # PERFORMANCE: das Parsen laeuft in EINEM awk-Prozess. Eine Vorgaengerversion
# Er wird in $STATE_DIR/cursor gemerkt -> keine Duplikate, kein Verlust. # 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: # Telegraf:
# [[inputs.exec]] # [[inputs.exec]]
# interval = "30s" # interval = "30s"
# timeout = "25s" # timeout = "28s"
# data_format = "influx" # data_format = "influx"
# commands = ["/etc/telegraf/journal-tail.sh"] # 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 set -u
PRIORITY="${JOURNAL_PRIORITY:-warning}" # warning|err|info|debug PRIORITY="${JOURNAL_PRIORITY:-warning}"
# Hartes Zeitbudget: Telegraf bricht den exec sonst ab (timeout 25s bei MAX_LINES="${JOURNAL_MAX_LINES:-500}"
# interval 30s). Wir lesen nie laenger als BUDGET Sekunden pro Aufruf — der
# Cursor macht den Rest beim naechsten Lauf.
BUDGET="${JOURNAL_BUDGET:-15}" BUDGET="${JOURNAL_BUDGET:-15}"
SINCE="${JOURNAL_SINCE:-}" # optional, nur beim Erstanlauf SINCE="${JOURNAL_SINCE:-}"
MAX_LINES="${JOURNAL_MAX_LINES:-500}" # Schutz vor Flut pro Lauf
STATE_DIR="${JOURNAL_STATE_DIR:-/var/lib/telegraf-journal}" STATE_DIR="${JOURNAL_STATE_DIR:-/var/lib/telegraf-journal}"
CURSOR_FILE="$STATE_DIR/cursor" CURSOR_FILE="$STATE_DIR/cursor"
mkdir -p "$STATE_DIR" 2>/dev/null || true 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 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 elif [ -n "$SINCE" ]; then
args+=(--since "$SINCE") START_ARGS=(--since "$SINCE")
else else
args+=(--since "-5min") START_ARGS=(--since "-5min")
fi 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 function emit( cur, msg, unit, ident, prio, ts, lhost, ns) {
# brauchen als Telegrafs timeout erlaubt. cur = jget($0, "__CURSOR")
timeout -k 2 "$BUDGET" journalctl "${args[@]}" 2>/dev/null | while IFS= read -r line; do if (cur != "") last = cur
case "$line" in \{*) ;; *) continue ;; esac
# Felder aus dem JSON ziehen (jq-frei, damit kein Extra-Paket nötig ist) msg = jget($0, "MESSAGE")
get() { printf '%s' "$line" | sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" | head -1; } if (msg == "") return
getnum() { printf '%s' "$line" | sed -n "s/.*\"$1\":\([0-9]*\).*/\1/p" | head -1; }
cur="$(get '__CURSOR')" unit = jget($0, "_SYSTEMD_UNIT"); if (unit == "") unit = jget($0, "SYSLOG_IDENTIFIER")
[ -n "$cur" ] && last_cursor="$cur" ident = jget($0, "SYSLOG_IDENTIFIER")
prio = jget($0, "PRIORITY")
ts = jget($0, "__REALTIME_TIMESTAMP")
lhost = jget($0, "_HOSTNAME")
msg="$(get 'MESSAGE')" if (unit == "") unit = "unknown"
[ -n "$msg" ] || continue if (ident == "") ident = "unknown"
if (prio == "") prio = "0"
if (lhost == "") lhost = "unknown"
unit="$(get '_SYSTEMD_UNIT')"; [ -n "$unit" ] || unit="$(get 'SYSLOG_IDENTIFIER')" # __REALTIME_TIMESTAMP ist in Mikrosekunden -> Nanosekunden
ident="$(get 'SYSLOG_IDENTIFIER')" ns = (ts ~ /^[0-9]+$/) ? ts "000" : sprintf("%.0f000000000", systime())
prio="$(get 'PRIORITY')"
ts_us="$(getnum '__REALTIME_TIMESTAMP')"
host="$(get '_HOSTNAME')"
# Line Protocol: Escaping für Tag-Werte und String-Felder # Tag heisst jhost, NICHT host: Telegraf setzt host=<node> als global_tag,
esc_tag() { printf '%s' "$1" | sed 's/[ ,=]/\\&/g'; } # sonst filtert das Dashboard-Log-Panel nach dem falschen Knoten.
esc_str() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; } 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 (substr($0, 1, 1) == "{") emit() }
if [ -n "$ts_us" ]; then
ns="${ts_us}000"
else
ns="$(date +%s)000000000"
fi
# Tag "jhost" statt "host": Telegrafs global_tag host=<node> hat Vorrang behalten, END {
# sonst filtert das Dashboard-Log-Panel nach dem falschen Knoten. # Cursor einmal am Ende sichern — atomar ersetzt das aufrufende Shell-Skript.
printf 'journal,unit=%s,ident=%s,priority=%s,jhost=%s message="%s" %s\n' \ if (last != "") { printf "%s", last > TMP; close(TMP) }
"$(esc_tag "${unit:-unknown}")" \ }
"$(esc_tag "${ident:-unknown}")" \ '
"$(esc_tag "${prio:-0}")" \
"$(esc_tag "${host:-unknown}")" \
"$(esc_str "$msg")" \
"$ns"
# Zustand nach jedem Datensatz sichern if [ -s "$CURSOR_FILE.tmp" ]; then
[ -n "$cur" ] && { printf '%s' "$cur" > "$CURSOR_FILE.tmp" && mv -f "$CURSOR_FILE.tmp" "$CURSOR_FILE"; } mv -f "$CURSOR_FILE.tmp" "$CURSOR_FILE" 2>/dev/null || true
done fi