"""WebSocket endpoint for real-time log streaming. Clients connect to /ws/logs and receive live log lines from all modules. """ from __future__ import annotations import asyncio import threading from collections import deque from fastapi import APIRouter, WebSocket, WebSocketDisconnect from Project.Sanad.core.logger import set_ws_push router = APIRouter() MAX_WATCHERS = 50 # Ring buffer of recent log lines (shared across connections). _recent: deque[str] = deque(maxlen=500) _watchers: set[asyncio.Queue] = set() _watchers_lock = threading.Lock() def push_log_line(line: str): """Called from the logging system to feed new lines. May be called from any thread (logging is multi-threaded), so we snapshot the watchers under a lock before iterating. """ _recent.append(line) with _watchers_lock: snapshot = list(_watchers) for q in snapshot: try: q.put_nowait(line) except asyncio.QueueFull: # Drop on overflow rather than block — logs are not critical data pass # Register with the logger so all log records are pushed to WS clients. # Wrap so a logger registration failure doesn't break Dashboard import. try: set_ws_push(push_log_line) except Exception: pass @router.websocket("/ws/logs") async def log_ws(ws: WebSocket): await ws.accept() with _watchers_lock: if len(_watchers) >= MAX_WATCHERS: await ws.close(code=1013, reason="Too many log watchers") return queue: asyncio.Queue[str] = asyncio.Queue(maxsize=200) _watchers.add(queue) try: # Send recent history for line in list(_recent): await ws.send_text(line) while True: line = await queue.get() await ws.send_text(line) except WebSocketDisconnect: pass except Exception: # Any other error closes the connection cleanly try: await ws.close() except Exception: pass finally: with _watchers_lock: _watchers.discard(queue)