Files
guyg1474andClaude Opus 5.5 c6eef176a2 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>
2026-10-14 15:15:01 +02:00

109 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()