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
132 lines
4.2 KiB
Python
Executable File
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)
|