scm-harmonic-cmts-admin/routers/logs.py

80 lines
2.9 KiB
Python
Raw Normal View History

# --- routers/logs.py ---
import asyncio
import logging
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from logger import ColoredFormatter, register_websocket_handler
router = APIRouter()
class LogBroadcaster:
"""管理所有活躍 WebSocket 連線的廣播器 (Connection Manager)"""
def __init__(self):
self.active_connections: set[WebSocket] = set()
self.loop: asyncio.AbstractEventLoop = None
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.add(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.discard(websocket)
async def broadcast(self, message: str):
"""非同步推播日誌給所有連線中的客戶端"""
if not self.active_connections:
return
# 併發發送,並透過 return_exceptions=True 確保單一連線異常不影響其他客戶端
tasks = [self._safe_send(conn, message) for conn in self.active_connections]
await asyncio.gather(*tasks, return_exceptions=True)
async def _safe_send(self, websocket: WebSocket, message: str):
try:
await websocket.send_text(message)
except Exception:
self.disconnect(websocket)
# 建立全域廣播器實例
log_broadcaster = LogBroadcaster()
class WebSocketLogHandler(logging.Handler):
"""自訂 Log Handler攔截系統日誌並安全地派發至非同步廣播器"""
def __init__(self, broadcaster: LogBroadcaster):
super().__init__()
self.broadcaster = broadcaster
# 沿用系統 ColoredFormatter完美保留 ANSI 色碼
self.setFormatter(ColoredFormatter())
def emit(self, record):
try:
msg = self.format(record)
loop = self.broadcaster.loop
# 確保在 Event Loop 處於執行狀態時,安全地跨執行緒派發任務
if loop and loop.is_running():
loop.call_soon_threadsafe(
lambda: asyncio.create_task(self.broadcaster.broadcast(msg))
)
except Exception:
self.handleError(record)
# 建立 Handler 實例並註冊至所有受管控的 Logger
websocket_log_handler = WebSocketLogHandler(log_broadcaster)
register_websocket_handler(websocket_log_handler)
@router.websocket("/ws/logs")
async def websocket_logs(websocket: WebSocket):
"""WebSocket 實時日誌串流端點"""
# 若尚未綁定 Event Loop則於首次連線時動態綁定
if not log_broadcaster.loop:
log_broadcaster.loop = asyncio.get_running_loop()
await log_broadcaster.connect(websocket)
try:
# 保持連線,監聽客戶端斷線狀態
while True:
await websocket.receive_text()
except WebSocketDisconnect:
log_broadcaster.disconnect(websocket)
except Exception:
log_broadcaster.disconnect(websocket)