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
+155
@@ -0,0 +1,155 @@
|
||||
#!/bin/sh
|
||||
# Installiert die llama-swap-Telemetrie als NUTZER-Instanz von Telegraf.
|
||||
#
|
||||
# WOFUER: Knoten, auf denen llama-swap laeuft, aber kein sudo existiert (ROG).
|
||||
# Die System-Instanz (install.sh) kann hier nicht greifen: sie schreibt nach
|
||||
# /etc/telegraf/telegraf.d und laeuft als root-Dienst. Ausserdem liest sie das
|
||||
# Systemjournal mit priority>=warning — llama-swaps INFO-Zeilen (Modellstart,
|
||||
# Ports) liegen im NUTZER-Journal und wurden deshalb nie nach Loki geschickt.
|
||||
# Diese Instanz darf `journalctl --user-unit` lesen und braucht kein root.
|
||||
#
|
||||
# ZUGANGSDATEN: kommen NICHT in die Konfiguration. Das Systemd-Unit setzt
|
||||
# EnvironmentFile=/etc/default/telegraf; Telegraf expandiert ${PROM_URL},
|
||||
# ${LOKI_URL} und ${TELEGRAF_TOKEN} beim Laden. Damit bleibt
|
||||
# /etc/default/telegraf (das der System-Installer geschrieben hat) die einzige
|
||||
# Quelle pro Knoten.
|
||||
#
|
||||
# BENUTZUNG (auf dem Zielknoten, als der Nutzer, dem llama-swap gehoert):
|
||||
# sh install-llamaswap-user.sh # Dateien liegen neben dem Skript
|
||||
# sh install-llamaswap-user.sh --raw <basis> # sonst von dort laden
|
||||
# sh install-llamaswap-user.sh --dry-run # nur zeigen, nichts schreiben
|
||||
#
|
||||
# IDEMPOTENT: mehrfaches Ausfuehren ist erlaubt; Unit wird neu geladen und
|
||||
# neu gestartet. Der Journal-Cursor bleibt erhalten (kein doppelter Versand).
|
||||
|
||||
set -eu
|
||||
|
||||
RAW_BASE=""
|
||||
DRY=0
|
||||
HIER="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--raw) RAW_BASE="$2"; shift 2 ;;
|
||||
--dry-run) DRY=1; shift ;;
|
||||
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
|
||||
*) printf 'Unbekannte Option: %s\n' "$1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
BIN_DIR="$HOME/bin"
|
||||
CONF_DIR="$HOME/.config/telegraf-llamaswap"
|
||||
UNIT_DIR="$HOME/.config/systemd/user"
|
||||
STATE_DIR="$HOME/.local/state/llamaswap-logs"
|
||||
|
||||
sag() { printf ' %s\n' "$*"; }
|
||||
schritt() { printf '\n== %s\n' "$*"; }
|
||||
|
||||
# ---------------------------------------------------------------- Vorbedingungen
|
||||
schritt "Vorbedingungen"
|
||||
if [ ! -r /etc/default/telegraf ]; then
|
||||
printf 'FEHLER: /etc/default/telegraf fehlt oder ist nicht lesbar.\n' >&2
|
||||
printf ' Ohne PROM_URL/LOKI_URL/TELEGRAF_TOKEN gibt es kein Ziel.\n' >&2
|
||||
printf ' Zuerst den Knoten-Installer laufen lassen (install.sh).\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
for var in NODE_NAME PROM_URL LOKI_URL TELEGRAF_TOKEN; do
|
||||
if ! grep -q "^${var}=" /etc/default/telegraf; then
|
||||
printf 'FEHLER: %s fehlt in /etc/default/telegraf.\n' "$var" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
sag "/etc/default/telegraf vorhanden (NODE_NAME, PROM_URL, LOKI_URL, TELEGRAF_TOKEN)"
|
||||
|
||||
TGBIN=""
|
||||
for kandidat in /usr/bin/telegraf /usr/local/bin/telegraf; do
|
||||
[ -x "$kandidat" ] && TGBIN="$kandidat" && break
|
||||
done
|
||||
[ -n "$TGBIN" ] || { printf 'FEHLER: telegraf-Binary nicht gefunden.\n' >&2; exit 1; }
|
||||
sag "Telegraf: $TGBIN ($("$TGBIN" --version | head -1))"
|
||||
|
||||
# ------------------------------------------------------------------- Dateien holen
|
||||
hole() { # hole <relativer pfad im repo> <ziel>
|
||||
_rel="$1"; _dst="$2"
|
||||
if [ -f "$HIER/../$_rel" ]; then
|
||||
cp "$HIER/../$_rel" "$_dst"; sag "aus Repo: $_rel"
|
||||
elif [ -n "$RAW_BASE" ]; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL -m 60 "$RAW_BASE/$_rel" -o "$_dst"; sag "geladen: $_rel"
|
||||
else
|
||||
printf 'FEHLER: curl fehlt und $_rel nicht lokal vorhanden.\n' >&2; exit 1
|
||||
fi
|
||||
else
|
||||
printf 'FEHLER: %s weder lokal noch --raw gesetzt.\n' "$_rel" >&2; exit 1
|
||||
fi
|
||||
[ -s "$_dst" ] || { printf 'FEHLER: %s ist leer.\n' "$_dst" >&2; exit 1; }
|
||||
}
|
||||
|
||||
if [ "$DRY" -eq 1 ]; then
|
||||
schritt "Trockenlauf — es wird nichts geschrieben"
|
||||
sag "wuerde anlegen: $BIN_DIR/llamaswap-telemetrie.py, $BIN_DIR/llamaswap-logs.py"
|
||||
sag "wuerde anlegen: $CONF_DIR/telegraf.conf (0600)"
|
||||
sag "wuerde anlegen: $UNIT_DIR/telegraf-llamaswap.service"
|
||||
sag "wuerde starten: systemctl --user enable --now telegraf-llamaswap.service"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
schritt "Programme nach $BIN_DIR"
|
||||
mkdir -p "$BIN_DIR" "$CONF_DIR" "$UNIT_DIR" "$STATE_DIR"
|
||||
hole scripts/llamaswap-telemetrie.py "$BIN_DIR/llamaswap-telemetrie.py"
|
||||
hole scripts/llamaswap-logs.py "$BIN_DIR/llamaswap-logs.py"
|
||||
chmod 755 "$BIN_DIR/llamaswap-telemetrie.py" "$BIN_DIR/llamaswap-logs.py"
|
||||
|
||||
schritt "Konfiguration nach $CONF_DIR"
|
||||
hole telegraf/llamaswap-user.conf "$CONF_DIR/telegraf.conf"
|
||||
chmod 600 "$CONF_DIR/telegraf.conf" # 0600: hier stehen keine Geheimnisse,
|
||||
# aber die Datei ist auch nicht oeffentlich
|
||||
|
||||
schritt "Unit nach $UNIT_DIR"
|
||||
hole telegraf/llamaswap-user.service "$UNIT_DIR/telegraf-llamaswap.service"
|
||||
|
||||
schritt "Probelauf der beiden Programme (vor dem Dienst)"
|
||||
set -a; . /etc/default/telegraf; set +a
|
||||
metriken="$(python3 "$BIN_DIR/llamaswap-telemetrie.py" | wc -l)"
|
||||
logs="$(python3 "$BIN_DIR/llamaswap-logs.py" | wc -l)"
|
||||
sag "llamaswap-telemetrie.py: $metriken Zeilen"
|
||||
sag "llamaswap-logs.py: $logs Zeilen (0 ist normal, wenn gerade nichts passiert)"
|
||||
if [ "$metriken" -eq 0 ]; then
|
||||
sag "WARNUNG: keine Metriken — laeuft llama-swap auf diesem Knoten?"
|
||||
fi
|
||||
|
||||
schritt "Telegraf-Konfiguration pruefen (--test, keine Ausgabe an die Ziele)"
|
||||
if ! "$TGBIN" --config "$CONF_DIR/telegraf.conf" --test --test-wait 5 >/dev/null 2>&1; then
|
||||
printf 'FEHLER: Telegraf meldet einen Konfigurationsfehler.\n' >&2
|
||||
"$TGBIN" --config "$CONF_DIR/telegraf.conf" --test --test-wait 5 2>&1 | tail -5 >&2
|
||||
exit 1
|
||||
fi
|
||||
sag "Konfiguration in Ordnung"
|
||||
|
||||
schritt "Dienst starten"
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now telegraf-llamaswap.service
|
||||
sleep 6
|
||||
zustand="$(systemctl --user is-active telegraf-llamaswap.service)"
|
||||
sag "telegraf-llamaswap.service: $zustand"
|
||||
[ "$zustand" = "active" ] || {
|
||||
printf 'FEHLER: Dienst laeuft nicht. Log:\n' >&2
|
||||
journalctl --user -u telegraf-llamaswap.service -n 15 --no-pager >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cat <<'HINWEIS'
|
||||
|
||||
== Fertig. Was jetzt laeuft
|
||||
* llama-swap /metrics -> Prometheus (Host-, GPU-Werte)
|
||||
* llamaswap_activity_* -> Token/Anfragen je Modell
|
||||
* llamacpp_* -> Token je laufendem Profil (nur mit --metrics)
|
||||
* Nutzer-Journal der Units -> Loki (unit="llama-swap.service")
|
||||
|
||||
Ohne `linger` endet der Dienst mit der Sitzung des Nutzers:
|
||||
loginctl enable-linger $USER (braucht meist root, einmalig)
|
||||
|
||||
Pruefen:
|
||||
systemctl --user status telegraf-llamaswap.service
|
||||
journalctl --user -u telegraf-llamaswap.service -n 20
|
||||
HINWEIS
|
||||
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)
|
||||
Executable
+214
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""llama-swap + llama.cpp -> Prometheus-Text fuer Telegraf (inputs.exec).
|
||||
|
||||
Warum dieses Skript ueberhaupt:
|
||||
llama-swap vergibt die Upstream-Ports DYNAMISCH (`${PORT}`, startPort 9340,
|
||||
stabile Modell-ID-Reihenfolge) - ein statischer Scrape kann sie nicht
|
||||
treffen. Ausserdem liegen die Token-Zahlen in zwei Quellen: llama.cpp
|
||||
(/metrics, nur mit --metrics, kein Modell-Label) und llama-swaps eigenem
|
||||
Statistik-Store (/api/metrics/stats, das die Upstream-Antworten selbst
|
||||
auswertet und damit auch strata und sd-server abdeckt).
|
||||
Ein exec-Input, das llama-swap selbst fragt, ist deshalb der einzige Weg
|
||||
ohne Raten.
|
||||
|
||||
Quellen (alle nur lesend, jede mit eigenem Timeout, Fehler brechen nichts ab):
|
||||
* GET /running laufende Modelle + Upstream-Adresse
|
||||
* GET /v1/models alle Modell-IDs
|
||||
* GET /api/metrics/stats?model=<id> Token-Summen je Modell
|
||||
* GET http://<upstream>/metrics llama.cpp-Zaehler je Profil
|
||||
|
||||
Ausgabe: Prometheus-Textformat auf stdout.
|
||||
* Keine erreichbare Quelle = keine Zeile fuer diese Quelle.
|
||||
* Es wird nie ein Wert erfunden und nie ein Teil-Fehler als 0 gemeldet.
|
||||
* Rueckgabewert ist immer 0, damit Telegraf den Lauf nicht als Fehler sieht.
|
||||
|
||||
Aufruf durch Telegraf (Nutzer-Instanz auf dem ROG):
|
||||
[[inputs.exec]]
|
||||
commands = ["/home/mm/bin/llamaswap-telemetrie.py"]
|
||||
data_format = "prometheus"
|
||||
timeout = "25s"
|
||||
interval = "60s"
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
BASE = os.environ.get("LLAMASWAP_BASE", "http://192.168.188.184:9300")
|
||||
TIMEOUT = float(os.environ.get("LLAMASWAP_TIMEOUT", "4"))
|
||||
|
||||
# Profile, die model-manager.sh auf festen Ports startet (Dauerlaeufer und
|
||||
# Handstarts). Sie erscheinen NICHT in /running, wenn llama-swap sie nicht
|
||||
# verwaltet - deshalb fest hinterlegt. Nicht erreichbare Ports kosten nur einen
|
||||
# sofortigen ECONNREFUSED, keine Wartezeit.
|
||||
FESTE_PORTS = {
|
||||
8101: "chat",
|
||||
8102: "embedding",
|
||||
8103: "vision",
|
||||
8104: "ocr",
|
||||
8105: "rerank",
|
||||
8107: "fast",
|
||||
8108: "agent",
|
||||
8109: "desktop",
|
||||
}
|
||||
|
||||
METRIK_RE = re.compile(r"^([a-zA-Z_:][a-zA-Z0-9_:]*)(\{[^}]*\})?\s+(\S+)\s*$")
|
||||
|
||||
|
||||
def hole(pfad, timeout=None):
|
||||
"""GET gegen llama-swap. Gibt (status, bytes) zurueck - status 0 bei Fehler."""
|
||||
url = pfad if pfad.startswith("http") else BASE + pfad
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=timeout or TIMEOUT) as antwort:
|
||||
return antwort.status, antwort.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, b""
|
||||
except Exception:
|
||||
return 0, b""
|
||||
|
||||
|
||||
def hole_json(pfad):
|
||||
status, rohdaten = hole(pfad)
|
||||
if status != 200 or not rohdaten:
|
||||
return None
|
||||
try:
|
||||
return json.loads(rohdaten)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def name_saeubern(name):
|
||||
"""llama.cpp liefert `llamacpp:prompt_tokens_total` (Doppelpunkt erlaubt,
|
||||
aber Prometheus reserviert ihn fuer Recording-Regeln) -> Unterstrich."""
|
||||
return name.replace(":", "_")
|
||||
|
||||
|
||||
def doppelpunkt_label_frei(labels):
|
||||
"""Label-String normalisieren: fuehrende/abschliessende Klammern entfernen."""
|
||||
if not labels:
|
||||
return ""
|
||||
labels = labels.strip()
|
||||
if labels.startswith("{") and labels.endswith("}"):
|
||||
return labels[1:-1]
|
||||
return ""
|
||||
|
||||
|
||||
def upstream_kandidaten():
|
||||
"""{port: modell} aus /running plus die festen model-manager-Ports."""
|
||||
kandidaten = dict(FESTE_PORTS)
|
||||
running = hole_json("/running")
|
||||
if isinstance(running, dict):
|
||||
for eintrag in running.get("running", []) or []:
|
||||
modell = eintrag.get("model") or ""
|
||||
proxy = eintrag.get("proxy") or ""
|
||||
m = re.search(r":(\d+)\s*$", proxy)
|
||||
if m and modell:
|
||||
# /running gewinnt: llama-swap kennt den echten Modellnamen.
|
||||
kandidaten[int(m.group(1))] = modell
|
||||
return kandidaten
|
||||
|
||||
|
||||
def llama_cpp_zeilen(port, modell):
|
||||
"""Prometheus-Zeilen eines llama.cpp-Upstreams, mit model-Label ergaenzt."""
|
||||
status, rohdaten = hole("http://192.168.188.184:%d/metrics" % port, timeout=2)
|
||||
if status != 200 or not rohdaten:
|
||||
return [], []
|
||||
text = rohdaten.decode("utf-8", "replace")
|
||||
if "llamacpp" not in text: # kein llama.cpp (strata, sd-server)
|
||||
return [], []
|
||||
|
||||
typen, zeilen = {}, []
|
||||
for zeile in text.splitlines():
|
||||
zeile = zeile.strip()
|
||||
if not zeile:
|
||||
continue
|
||||
if zeile.startswith("# TYPE"):
|
||||
teile = zeile.split()
|
||||
if len(teile) >= 4:
|
||||
typen[name_saeubern(teile[2])] = teile[3]
|
||||
continue
|
||||
if zeile.startswith("#"):
|
||||
continue
|
||||
m = METRIK_RE.match(zeile)
|
||||
if not m:
|
||||
continue
|
||||
name = name_saeubern(m.group(1))
|
||||
labels = doppelpunkt_label_frei(m.group(2))
|
||||
wert = m.group(3)
|
||||
if wert in ("NaN", "+Inf", "-Inf"):
|
||||
continue
|
||||
try:
|
||||
float(wert)
|
||||
except ValueError:
|
||||
continue
|
||||
neue_labels = ('model="%s"' % modell) + ("," + labels if labels else "")
|
||||
zeilen.append((name, neue_labels, wert))
|
||||
return typen, zeilen
|
||||
|
||||
|
||||
def aktivitaets_zeilen():
|
||||
"""Token-Summen je Modell aus llama-swaps Statistik-Store."""
|
||||
modelle = hole_json("/v1/models")
|
||||
if not isinstance(modelle, dict):
|
||||
return [], []
|
||||
ids = [m.get("id") for m in modelle.get("data", []) or [] if m.get("id")]
|
||||
felder = [
|
||||
("total_requests", "llamaswap_activity_requests_total"),
|
||||
("total_input_tokens", "llamaswap_activity_input_tokens_total"),
|
||||
("total_output_tokens", "llamaswap_activity_output_tokens_total"),
|
||||
("total_cache_tokens", "llamaswap_activity_cache_tokens_total"),
|
||||
]
|
||||
zeilen = []
|
||||
for modell in ids:
|
||||
stats = hole_json("/api/metrics/stats?model=%s" % urllib.parse.quote(modell))
|
||||
if not isinstance(stats, dict):
|
||||
continue
|
||||
for schluessel, metrik in felder:
|
||||
wert = stats.get(schluessel)
|
||||
if isinstance(wert, (int, float)) and not isinstance(wert, bool):
|
||||
zeilen.append((metrik, 'model="%s"' % modell, "%d" % wert))
|
||||
return zeilen
|
||||
|
||||
|
||||
def main():
|
||||
typen = {}
|
||||
zeilen = []
|
||||
|
||||
kandidaten = upstream_kandidaten()
|
||||
for port, modell in sorted(kandidaten.items()):
|
||||
t, z = llama_cpp_zeilen(port, modell)
|
||||
typen.update(t)
|
||||
zeilen.extend(z)
|
||||
|
||||
aktivitaet = aktivitaets_zeilen()
|
||||
for metrik, _, _ in aktivitaet:
|
||||
typen[metrik] = "counter"
|
||||
zeilen.extend(aktivitaet)
|
||||
|
||||
if not zeilen:
|
||||
# Nichts erreichbar: lieber gar nichts ausgeben als Nullen.
|
||||
return 0
|
||||
|
||||
ausgabe = []
|
||||
for name in sorted({n for n, _, _ in zeilen}):
|
||||
typ = typen.get(name, "gauge")
|
||||
ausgabe.append("# TYPE %s %s" % (name, typ))
|
||||
for n, labels, wert in zeilen:
|
||||
if n != name:
|
||||
continue
|
||||
ausgabe.append("%s{%s} %s" % (n, labels, wert))
|
||||
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-telemetrie: %s\n" % fehler)
|
||||
sys.exit(0)
|
||||
Reference in new issue
Block a user