""" Ring-Buffer + Pub/Sub fuer das Live-'Verbindungslog' im Admin-Bereich. Haengt sich als zusaetzlicher logging.Handler an den Root-Logger, haelt die letzten LOG-Zeilen im Prozessspeicher (fuer den initialen Replay beim Oeffnen des Tabs) und verteilt jede neue Zeile an alle aktuell verbundenen WebSocket-Abonnenten (siehe app/admin/routes.py, WS /admin/ws/logs). Bewusst KEINE Persistenz in der DB -- das ist ein reiner Live-Tail (wie 'journalctl -f'), kein durchsuchbares Archiv. Rein prozesslokal: bei mehreren Worker-Prozessen sieht ein Abonnent nur die Logs des Worker-Prozesses, der seine WebSocket-Verbindung bedient. """ from __future__ import annotations import asyncio import logging from collections import deque MAX_BUFFER_LINES = 1000 _FORMATTER = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s") _buffer: deque[str] = deque(maxlen=MAX_BUFFER_LINES) _subscribers: set[asyncio.Queue[str]] = set() _installed = False class _BroadcastHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: try: line = _FORMATTER.format(record) except Exception: return _buffer.append(line) for queue in list(_subscribers): try: queue.put_nowait(line) except asyncio.QueueFull: # Langsamer/verschwundener Abonnent -- lieber eine Zeile # verlieren als das Event-Loop-Logging blockieren. pass def install() -> None: """Idempotent -- an app.main::lifespan gebunden. Haengt den Handler an den Root-Logger (sieht alles, was propagiert) und hebt den Level des eigenen App-Namespace 'jumphost' auf DEBUG an, damit die Verbindungs- Debug-Meldungen (SSH/RDP-Handshake etc.) ueberhaupt erzeugt werden, ohne Drittanbieter-Logger (uvicorn, asyncssh) unnoetig auf DEBUG zu stellen.""" global _installed if _installed: return handler = _BroadcastHandler() handler.setLevel(logging.DEBUG) logging.getLogger().addHandler(handler) logging.getLogger("jumphost").setLevel(logging.DEBUG) _installed = True def subscribe() -> asyncio.Queue[str]: queue: asyncio.Queue[str] = asyncio.Queue(maxsize=500) _subscribers.add(queue) return queue def unsubscribe(queue: asyncio.Queue[str]) -> None: _subscribers.discard(queue) def recent_lines() -> list[str]: return list(_buffer)