more admin stuff 2

This commit is contained in:
2026-08-20 17:10:19 +02:00
parent e8216b14e9
commit cd5957cbd2
19 changed files with 1120 additions and 78 deletions

View File

@ -0,0 +1,71 @@
"""
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)