more admin stuff 2
This commit is contained in:
71
app/security/log_stream.py
Normal file
71
app/security/log_stream.py
Normal 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)
|
||||
Reference in New Issue
Block a user