perf(logs): journal-tail parst in einem awk-Durchlauf (11s -> 143ms, exec-Timeout behoben)
This commit is contained in:
1 parent
b49f9ccbfd
commit
8ca06bd6d6
1 file changed
+80
-55
+80
-55
@@ -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
|
||||||
Reference in new issue
Block a user