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:
mm committed 2026-10-10 22:25:18 +02:00
1 parent 59ab6a9a22
commit 235cbc68dc
6 files changed
+646

No files matched your search

+42
View File
@@ -545,6 +545,48 @@ Wer kein sudo hat, aber in der Gruppe `docker` ist: Container-Variante aus
[netbird-observability/stacks/telegraf-node](https://gitea.m-m.bio/mm/netbird-observability) (privates Repo).
Dort ist `network_mode: host` gesetzt — ohne das sieht Telegraf nur `lo`+`eth0` statt der echten Interfaces.
## llama-swap-Telemetrie ohne sudo (Nutzer-Instanz)
Für Knoten, die eine lokale Inferenz-Landschaft über **llama-swap** anbieten und
**kein sudo** haben (ROG). Die System-Instanz dieses Installers kann dort nicht
greifen — und sie würde llama-swap auch nicht sichtbar machen: llama-swap läuft
als **User**-Unit, seine INFO-Zeilen liegen im Nutzer-Journal, und
`journal-tail.sh` liest das Systemjournal mit `priority >= warning`. Ergebnis
vorher: null llama-swap-Zeilen in Loki.
```sh
# auf dem Zielknoten, als der Nutzer, dem llama-swap gehört
sh scripts/install-llamaswap-user.sh
# Optionen: --raw <basis-url> (Dateien vom Repo laden) --dry-run
```
Was die Nutzer-Instanz holt:
| Quelle | Weg | Inhalt |
|---|---|---|
| `llama-swap:9300/metrics` | `inputs.prometheus` | Host (CPU je Kern, RAM, Netz), GPU (Auslastung, VRAM, Temperatur, Leistung) |
| `/api/metrics/stats?model=<id>` | `inputs.exec` → `llamaswap-telemetrie.py` | Token/Anfragen/Cache **je Modell** — auch für Modelle ohne `/metrics` (strata-Engine, sd-server) |
| `http://<upstream>/metrics` | dito | Token je **laufendem** Profil aus llama.cpp (nur mit `--metrics`) |
| `journalctl --user-unit …` | `inputs.exec` → `llamaswap-logs.py` | Logs von llama-swap und kalina-backends für Loki |
Voraussetzungen, die llama-swap selbst erfüllen muss:
* **`--metrics` in den Startbefehlen** — llama.cpp liefert `/metrics` nur mit
diesem Schalter (sonst HTTP 501). Für `model-manager.sh`, die Inline-`cmd`s in
`llama-swap.yaml` und eigene Startskripte.
* **`store.path` in `llama-swap.yaml`** — ohne Datei-Store hält llama-swap die
Aktivität nur im RAM und kürzt sie nach `metricsMaxInMemory`; Token-Summen
wären dann ein rollendes Fenster. Mit Store sind sie dauerhaft (SQLite).
Zugangsdaten bleiben dort, wo der Knoten-Installer sie hingelegt hat:
`EnvironmentFile=/etc/default/telegraf` für die Unit, `${PROM_URL}` /
`${LOKI_URL}` / `${TELEGRAF_TOKEN}` in der Konfiguration. Kein Kopieren von
Token. Ohne `linger` endet die Unit mit der Sitzung — `loginctl enable-linger $USER`.
Grafana: Dashboard **llama-swap (ROG)** (`uid: llama-swap-rog`) in
`netbird-observability/stacks/central-imac/dashboards/llama-swap.json`.
## Error-Logs (seit 1.3.0)
Apache schreibt **zwei** Logarten mit **verschiedenen** Formaten:
+155
View File
@@ -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
+131
View File
@@ -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)
+214
View File
@@ -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)
+82
View File
@@ -0,0 +1,82 @@
# llama-swap-Telemetrie des ROG — Nutzer-Instanz von Telegraf.
#
# Warum eine zweite Instanz und nicht /etc/telegraf/telegraf.d:
# Auf dem ROG gibt es kein sudo. llama-swap und kalina-backends laufen als
# Nutzer-Units; ihre Logs stehen im Nutzer-Journal und werden von der
# System-Instanz (journal-tail.sh, priority>=warning) verworfen. Eine
# Nutzer-Instanz darf --user-unit lesen und braucht keinen root.
#
# Zugangsdaten NICHT hier: das Systemd-Unit setzt
# EnvironmentFile=/etc/default/telegraf, Telegraf expandiert ${PROM_URL},
# ${LOKI_URL}, ${TELEGRAF_TOKEN} beim Laden. Eine Quelle fuer alle Knoten.
#
# Aufruf: telegraf --config /home/mm/.config/telegraf-llamaswap/telegraf.conf
[global_tags]
node = "ROG"
[agent]
interval = "15s"
flush_interval = "15s"
metric_batch_size = 1000
metric_buffer_limit = 20000
hostname = "ROG"
omit_hostname = false
round_interval = true
skip_processors_after_aggregators = true
# ---- Metriken → Prometheus Remote Write (imac) ----
[[outputs.http]]
url = "${PROM_URL}"
data_format = "prometheusremotewrite"
timeout = "10s"
namedrop = ["journal"]
[outputs.http.headers]
Content-Type = "application/x-protobuf"
Content-Encoding = "snappy"
X-Prometheus-Remote-Write-Version = "0.1.0"
Authorization = "Bearer ${TELEGRAF_TOKEN}"
# ---- Logs → Loki (imac) ----
[[outputs.loki]]
domain = "${LOKI_URL}"
endpoint = "/loki/api/v1/push"
timeout = "10s"
namepass = ["journal"]
sanitize_label_names = true
metric_name_label = ""
http_headers = { "Authorization" = "Bearer ${TELEGRAF_TOKEN}" }
# ---- 1. llama-swap selbst: System- und GPU-Werte ----
# /metrics liefert llama-swap nativ (CPU, RAM, Netz, GPU-Auslastung, VRAM,
# Temperatur, Leistungsaufnahme). Namen sind schon llamaswap_*.
[[inputs.prometheus]]
urls = ["http://192.168.188.184:9300/metrics"]
metric_version = 2
interval = "30s"
timeout = "10s"
[inputs.prometheus.tags]
app = "llama-swap"
# ---- 2. Token-/Request-Zaehler je Modell ----
# Ein exec statt fester Ports: llama-swap vergibt die Upstream-Ports dynamisch
# (${PORT}, startPort 9340) und nur laufende Modelle haben ueberhaupt eine
# /metrics. Das Skript fragt llama-swap selbst und ergaenzt das model-Label.
[[inputs.exec]]
commands = [["/home/mm/bin/llamaswap-telemetrie.py"]]
data_format = "prometheus"
interval = "60s"
timeout = "25s"
[inputs.exec.tags]
app = "llama-swap"
# ---- 3. Logs der Nutzer-Units (llama-swap, kalina-backends) ----
# Gleiche Zeilenform wie journal-tail.sh (measurement journal), damit die
# bestehenden Log-Panels ohne Sonderfall greifen.
[[inputs.exec]]
commands = [["/home/mm/bin/llamaswap-logs.py"]]
data_format = "influx"
interval = "30s"
timeout = "20s"
[inputs.exec.tags]
app = "llama-swap"
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=Telegraf — llama-swap-Telemetrie (ROG, Nutzer-Instanz)
Documentation=https://github.com/influxdata/telegraf
# Ohne llama-swap gibt es nichts zu holen; die Unit startet trotzdem, damit
# ein spaeterer llama-swap-Start nicht am Logger scheitert (After statt Requires).
After=network-online.target llama-swap.service
Wants=llama-swap.service
[Service]
Type=notify
NotifyAccess=all
# Dieselben Zugangsdaten wie die System-Instanz: eine Quelle pro Knoten,
# keine Kopie des Tokens in dieser Unit oder im Conf.
EnvironmentFile=/etc/default/telegraf
ExecStart=/usr/bin/telegraf --config /home/mm/.config/telegraf-llamaswap/telegraf.conf
Restart=on-failure
RestartForceExitStatus=SIGPIPE
KillMode=mixed
LimitMEMLOCK=8M:8M
[Install]
WantedBy=default.target