72 lines
2.4 KiB
Python
72 lines
2.4 KiB
Python
"""
|
|
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)
|