74 lines
2.2 KiB
Python
74 lines
2.2 KiB
Python
"""
|
|
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)
|