Files
netbird-telegraf-installer/scripts/llamaswap-logs.py
T
mm 235cbc68dc llama-swap-Telemetrie als Nutzer-Instanz (Knoten ohne sudo)
Warum: llama-swap laeuft als Nutzer-Unit. Der System-Telegraf liest das
Systemjournal mit priority>=warning und liest damit keine einzige
llama-swap-INFO-Zeile - Loki hatte fuer den ROG null Zeilen. Ausserdem kann
die System-Instanz auf einem Knoten ohne sudo nicht erweitert werden.

- scripts/llamaswap-telemetrie.py: Token/Anfragen je Modell aus llama-swaps
  Statistik-API plus llama.cpp /metrics je laufendem Profil (Ports sind
  dynamisch, deshalb kein statischer Scrape)
- scripts/llamaswap-logs.py: Nutzer-Journal (llama-swap, kalina-backends)
  nach Loki, gleiche Zeilenform wie journal-tail.sh
- telegraf/llamaswap-user.conf + .service: Zugangsdaten per
  EnvironmentFile=/etc/default/telegraf, keine Token-Kopie
- scripts/install-llamaswap-user.sh: idempotente Installation mit --dry-run
  und Konfigurationspruefung
2026-10-10 22:25:18 +02:00

132 lines
4.2 KiB
Python
Executable File

#!/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)