P42-S.M.: Server-Monitoring-Agent als Docker-Container

Misst CPU, RAM, Swap, Platten, Netzwerk, Temperaturen und Prozesse des
Hosts und liefert sie als JSON-API (/api/metrics, /api/history) für das
P42-Dashboard. Nur Python-Standardbibliothek, Token-geschützt,
Container ohne Root und mit schreibgeschütztem Dateisystem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-14 15:15:01 +02:00
commit c6eef176a2
11 files changed
+727

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
"""HTTP-API: liefert die zuletzt gemessenen Werte als JSON.
GET /api/health ohne Token – für Docker-Healthcheck und Erreichbarkeitsprüfung
GET /api/metrics aktueller Stand (Token nötig)
GET /api/history Verlauf, optional ?minutes=N (Token nötig)
"""
import hmac
import json
import logging
import os
import threading
import time
from collections import deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse
from . import collector
log = logging.getLogger("p42-sm")
TOKEN = os.environ.get("SM_TOKEN", "").strip()
INTERVAL = max(1, int(os.environ.get("SM_INTERVAL", "5")))
HISTORY = max(1, int(os.environ.get("SM_HISTORY", "720"))) # 720 × 5 s = 1 Stunde
_lock = threading.Lock()
_latest: dict | None = None
_history: deque = deque(maxlen=HISTORY)
_last_update = 0.0
def _collect_loop() -> None:
global _latest, _last_update
prev = collector.raw_sample()
time.sleep(1) # erste Messung schon nach 1 s, damit die API schnell Daten hat
while True:
try:
cur = collector.raw_sample()
snap = collector.snapshot(prev, cur, INTERVAL)
with _lock:
_latest = snap
_history.append(collector.history_point(snap))
_last_update = time.monotonic()
prev = cur
except Exception:
log.exception("Messung fehlgeschlagen")
time.sleep(INTERVAL)
class Handler(BaseHTTPRequestHandler):
server_version = "P42-SM"
sys_version = ""
def _send(self, status: int, body: dict) -> None:
data = json.dumps(body, ensure_ascii=False).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
def _authorized(self) -> bool:
if not TOKEN:
return True
header = self.headers.get("Authorization", "")
scheme, _, given = header.partition(" ")
return scheme.lower() == "bearer" and hmac.compare_digest(given.strip().encode(), TOKEN.encode())
def do_GET(self) -> None:
url = urlparse(self.path)
with _lock:
latest, history, age = _latest, list(_history), time.monotonic() - _last_update
if url.path == "/api/health":
# Ungesund, wenn die Messung hängt (drei Intervalle ohne neue Werte)
ok = latest is not None and age < INTERVAL * 3 + 5
return self._send(200 if ok else 503, {"status": "ok" if ok else "starting"})
if url.path not in ("/api/metrics", "/api/history"):
return self._send(404, {"error": "Nicht gefunden."})
if not self._authorized():
return self._send(401, {"error": "Token fehlt oder ist falsch."})
if latest is None:
return self._send(503, {"error": "Noch keine Messwerte – bitte gleich erneut versuchen."})
if url.path == "/api/metrics":
return self._send(200, latest)
minutes = parse_qs(url.query).get("minutes", [""])[0]
if minutes.isdigit():
history = history[-max(1, int(minutes) * 60 // INTERVAL):]
return self._send(200, {"intervalSeconds": INTERVAL, "points": history})
def log_message(self, fmt: str, *args) -> None:
# Nur Fehler loggen – der Abruf alle paar Sekunden würde das Log sonst fluten
if len(args) >= 2 and str(args[1]).startswith(("4", "5")):
log.warning("%s %s", self.address_string(), fmt % args)
def serve() -> None:
host = os.environ.get("HOST", "0.0.0.0")
port = int(os.environ.get("PORT", "9142"))
if not TOKEN:
log.warning("SM_TOKEN ist leer – die API ist ohne Anmeldung abrufbar!")
threading.Thread(target=_collect_loop, daemon=True, name="collector").start()
httpd = ThreadingHTTPServer((host, port), Handler)
log.info("P42-S.M. läuft auf %s:%s (Messintervall %s s, Verlauf %s Punkte)", host, port, INTERVAL, HISTORY)
httpd.serve_forever()