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
This commit is contained in:
1 parent
59ab6a9a22
commit
235cbc68dc
6 files changed
+646
No files matched your search
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/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)
|
||||
Reference in new issue
Block a user