""" This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course. © Copyright Utrecht University (Department of Information and Computing Sciences) """ import logging from pathlib import Path import zmq from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse, StreamingResponse from zmq.asyncio import Context from control_backend.core.config import settings logger = logging.getLogger(__name__) router = APIRouter() # DO NOT LOG INSIDE THIS FUNCTION @router.get("/logs/stream") async def log_stream(): """ Server-Sent Events (SSE) endpoint for real-time log streaming. Subscribes to the internal ZMQ logging topic and forwards log records to the client. Allows the frontend to display live logs from the backend. :return: A StreamingResponse yielding SSE data. """ context = Context.instance() socket = context.socket(zmq.SUB) for level in logging.getLevelNamesMapping(): socket.subscribe(topic=level) socket.connect(settings.zmq_settings.internal_sub_address) async def gen(): while True: _, message = await socket.recv_multipart() message = message.decode().strip() yield f"data: {message}\n\n" return StreamingResponse(gen(), media_type="text/event-stream") LOGGING_DIR = Path(settings.logging_settings.experiment_log_directory).resolve() @router.get("/logs/files") @router.get("/api/logs/files") async def log_directory(): """ Get a list of all log files stored in the experiment log file directory. """ return [f.name for f in LOGGING_DIR.glob("*.log")] @router.get("/logs/files/{filename}") @router.get("/api/logs/files/{filename}") async def log_file(filename: str): # Prevent path-traversal file_path = (LOGGING_DIR / filename).resolve() # This .resolve() is important if not file_path.is_relative_to(LOGGING_DIR): raise HTTPException(status_code=400, detail="Invalid filename.") if not file_path.is_file(): raise HTTPException(status_code=404, detail="File not found.") return FileResponse(file_path, filename=file_path.name)