feat: log download endpoints

ref: N25B-401
This commit is contained in:
Twirre Meulenbelt
2026-01-16 16:32:51 +01:00
parent 4cda4e5e70
commit ba79d09c5d
3 changed files with 34 additions and 4 deletions

View File

@@ -48,6 +48,7 @@ handlers:
class: control_backend.logging.DatedFileHandler class: control_backend.logging.DatedFileHandler
formatter: experiment formatter: experiment
filters: [partial] filters: [partial]
# Directory must match config.logging_settings.experiment_log_directory
file_prefix: experiment_logs/experiment file_prefix: experiment_logs/experiment
# Level for external libraries # Level for external libraries
@@ -59,6 +60,6 @@ loggers:
control_backend: control_backend:
level: LLM level: LLM
handlers: [ui] handlers: [ui]
experiment: experiment: # This name must match config.logging_settings.experiment_logger_name
level: DEBUG level: DEBUG
handlers: [ui, file] handlers: [ui, file]

View File

@@ -1,8 +1,9 @@
import logging import logging
from pathlib import Path
import zmq import zmq
from fastapi import APIRouter from fastapi import APIRouter, HTTPException
from fastapi.responses import StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from zmq.asyncio import Context from zmq.asyncio import Context
from control_backend.core.config import settings from control_backend.core.config import settings
@@ -38,3 +39,29 @@ async def log_stream():
yield f"data: {message}\n\n" yield f"data: {message}\n\n"
return StreamingResponse(gen(), media_type="text/event-stream") 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)

View File

@@ -147,10 +147,12 @@ class LoggingSettings(BaseModel):
Configuration for logging. Configuration for logging.
:ivar logging_config_file: Path to the logging configuration file. :ivar logging_config_file: Path to the logging configuration file.
:ivar experiment_logger_name: Name of the experiment logger, should match the logging config. :ivar experiment_log_directory: Location of the experiment logs. Must match the logging config.
:ivar experiment_logger_name: Name of the experiment logger. Must match the logging config.
""" """
logging_config_file: str = ".logging_config.yaml" logging_config_file: str = ".logging_config.yaml"
experiment_log_directory: str = "experiment_logs"
experiment_logger_name: str = "experiment" experiment_logger_name: str = "experiment"