#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Logs der llama-swap-Nutzer-Units -> Influx Line Protocol fuer Telegraf. Warum eigene Quelle: llama-swap und kalina-backends laufen als *User*-Units. Der Knoten-Telegraf liest das Systemjournal ueber journal-tail.sh und filtert dort zusaetzlich auf priority >= warning. Die INFO-Zeilen von llama-swap (Modellstart, Port-Vergabe, Weckpfad) und alles von kalina-backends kommen deshalb dort nie an - Loki hatte fuer den ROG keine einzige llama-swap-Zeile. Ein Nutzer-Prozess darf --user-unit lesen; damit ist kein sudo noetig. Ausgabe: dieselbe Zeilenform wie journal-tail.sh (measurement `journal`, Tags unit/ident/priority/jhost, Feld message), damit die bestehenden Log-Panels im Grafana ohne Sonderfall greifen. Zustand: __CURSOR der letzten Zeile in $STATE/cursor - keine Duplikate, kein Verlust bei Neustart oder Rotation. """ import json import os import subprocess import sys UNITS = os.environ.get( "LLAMASWAP_LOG_UNITS", "llama-swap.service,kalina-backends.service" ).split(",") PRIORITY = os.environ.get("LLAMASWAP_LOG_PRIORITY", "info") MAX_LINES = os.environ.get("LLAMASWAP_LOG_MAX_LINES", "800") BUDGET = float(os.environ.get("LLAMASWAP_LOG_BUDGET", "12")) SINCE = os.environ.get("LLAMASWAP_LOG_SINCE", "-5min") STATE = os.path.expanduser( os.environ.get("LLAMASWAP_LOG_STATE", "~/.local/state/llamaswap-logs") ) CURSOR = os.path.join(STATE, "cursor") def etag(wert): """Tag-Werte: Komma, Leerzeichen, Gleichheitszeichen escapen.""" for zeichen in ("\\", " ", ",", "="): wert = wert.replace(zeichen, "\\" + zeichen) return wert def estr(wert): """String-Feld: Backslash und Anfuehrungszeichen escapen.""" return wert.replace("\\", "\\\\").replace('"', '\\"') def journal_zeilen(): args = ["journalctl", "-o", "json", "--no-pager", "--no-hostname", "-p", PRIORITY, "-n", MAX_LINES] for unit in UNITS: unit = unit.strip() if unit: args += ["--user-unit", unit] try: os.makedirs(STATE, exist_ok=True) except OSError: return [] if os.path.exists(CURSOR): try: with open(CURSOR, "r", encoding="utf-8") as fh: letzter = fh.read().strip() except OSError: letzter = "" args += ["--after-cursor", letzter] if letzter else ["--since", SINCE] else: args += ["--since", SINCE] try: ergebnis = subprocess.run( args, capture_output=True, text=True, timeout=BUDGET ) except (subprocess.TimeoutExpired, OSError): return [] return [z for z in ergebnis.stdout.splitlines() if z.startswith("{")] def main(): ausgabe, neuer_cursor = [], "" for zeile in journal_zeilen(): try: eintrag = json.loads(zeile) except ValueError: continue neuer_cursor = eintrag.get("__CURSOR", neuer_cursor) nachricht = eintrag.get("MESSAGE", "") if not isinstance(nachricht, str) or not nachricht: continue unit = eintrag.get("_SYSTEMD_USER_UNIT") or eintrag.get("_SYSTEMD_UNIT") \ or eintrag.get("SYSLOG_IDENTIFIER") or "unknown" ident = eintrag.get("SYSLOG_IDENTIFIER") or "unknown" prio = str(eintrag.get("PRIORITY", "0")) jhost = eintrag.get("_HOSTNAME") or "unknown" ts = str(eintrag.get("__REALTIME_TIMESTAMP", "")) ns = ts + "000" if ts.isdigit() else "" if not ns: continue ausgabe.append( 'journal,unit=%s,ident=%s,priority=%s,jhost=%s message="%s" %s' % (etag(unit), etag(ident), etag(prio), etag(jhost), estr(nachricht), ns) ) if neuer_cursor: try: with open(CURSOR + ".tmp", "w", encoding="utf-8") as fh: fh.write(neuer_cursor) os.replace(CURSOR + ".tmp", CURSOR) except OSError: pass if ausgabe: sys.stdout.write("\n".join(ausgabe) + "\n") return 0 if __name__ == "__main__": try: sys.exit(main()) except Exception as fehler: # fail-open: nie den exec brechen sys.stderr.write("llamaswap-logs: %s\n" % fehler) sys.exit(0)