From 1046f9f13cc965c9344efdb54cd4648063b409a9 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Wed, 26 Aug 2026 10:00:48 +0200 Subject: [PATCH] Health-Metriken im Checkin + Jitter aufs Standardintervall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collect_health() sammelt Diskbelegung/RAM/Uptime/Load Average rein per Stdlib (shutil.disk_usage, /proc/meminfo, /proc/uptime, os.getloadavg), jeder Wert einzeln try/except. checkin() schickt das Ergebnis jetzt mit. main()-Loop wendet 0-30 Min Jitter nur auf 'große' Intervalle (>=1h) an - der kurze Debug-Modus (60s) und der Fallback-Retry bleiben unverjittert, sonst würde Jitter den Debug-Zweck untergraben. Boot-Checkin war schon vorher vorhanden (run_once() läuft immer vor der ersten sleep()), hier nicht angefasst. Co-Authored-By: Claude Opus 5 --- agent.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/agent.py b/agent.py index 58258c5..cf52727 100644 --- a/agent.py +++ b/agent.py @@ -1,5 +1,6 @@ import json import os +import random import shutil import subprocess import sys @@ -13,6 +14,12 @@ CREDENTIALS_PATH = os.environ.get( "TUXFLOTTE_AGENT_CREDENTIALS", "/etc/tuxflotte/agent.credentials" ) FALLBACK_INTERVAL_SECONDS = 60 +# Jitter (26.08.2026-Planung): nur auf "grosse" Intervalle (Standard 6h) +# anwenden, nicht auf den kurzen Debug-Modus (60s) oder den Fallback-Retry +# (FALLBACK_INTERVAL_SECONDS) - sonst wuerde Jitter genau den Zweck des +# schnellen Debug-Zyklus untergraben. +JITTER_THRESHOLD_SECONDS = 60 * 60 +JITTER_MAX_SECONDS = 30 * 60 def log(message): @@ -71,9 +78,55 @@ def bootstrap(device_id): return json.loads(response.read().decode("utf-8")) +def collect_health(): + """ + Health-Metriken fuers Checkin (26.08.2026-Planung) - reines Stdlib, + keine neue Abhaengigkeit. Jeder Einzelwert einzeln in try/except: ein + fehlschlagender Wert (z.B. Load Average auf einem ungewoehnlichen + System) darf die anderen nicht verhindern. + """ + + health = {} + + try: + nutzung = shutil.disk_usage("/") + health["disk_used_percent"] = round(nutzung.used / nutzung.total * 100, 1) + except OSError as exc: + log(f"Health: Diskbelegung nicht ermittelbar: {exc}") + + try: + werte = {} + with open("/proc/meminfo", "r", encoding="utf-8") as f: + for zeile in f: + schluessel, _, rest = zeile.partition(":") + if schluessel in ("MemTotal", "MemAvailable"): + werte[schluessel] = int(rest.strip().split()[0]) + if werte.get("MemTotal"): + belegt = werte["MemTotal"] - werte.get("MemAvailable", 0) + health["ram_used_percent"] = round(belegt / werte["MemTotal"] * 100, 1) + except (OSError, ValueError) as exc: + log(f"Health: RAM-Auslastung nicht ermittelbar: {exc}") + + try: + with open("/proc/uptime", "r", encoding="utf-8") as f: + health["uptime_seconds"] = int(float(f.read().split()[0])) + except (OSError, ValueError) as exc: + log(f"Health: Uptime nicht ermittelbar: {exc}") + + try: + load1, load5, load15 = os.getloadavg() + health["load_1m"] = round(load1, 2) + health["load_5m"] = round(load5, 2) + health["load_15m"] = round(load15, 2) + except OSError as exc: + log(f"Health: Load Average nicht ermittelbar: {exc}") + + return health + + def checkin(device_id, agent_secret): return api_request( - "/api/v1/agent/checkin", agent_secret, {"device_id": device_id} + "/api/v1/agent/checkin", agent_secret, {"device_id": device_id, "health": collect_health()} ) @@ -205,6 +258,9 @@ def main(): log(f"Check-in-Fehler: {exc}") interval = FALLBACK_INTERVAL_SECONDS + if interval >= JITTER_THRESHOLD_SECONDS: + interval += random.uniform(0, JITTER_MAX_SECONDS) + time.sleep(interval)