From d8e7cc7053f35b10da39042ce2b77b6eed298f8b Mon Sep 17 00:00:00 2001 From: Kfir Sadeh Date: Mon, 15 Jun 2026 11:32:51 +0300 Subject: [PATCH] feat(ui): add real-time diagnostic logs console (#974) * feat(diagnostics): add admin-gated real-time diagnostics logs terminal UI * feat(ui): resolve diagnostics logs feedback and optimize client-side caching * feat(ui): resolve diagnostics logs feedback --- app.py | 35 +++++- routes/diagnostics_routes.py | 27 ++++- static/index.html | 55 +++++++++ static/js/admin.js | 198 ++++++++++++++++++++++++++++++++- static/style.css | 99 +++++++++++++++++ tests/test_diagnostics_logs.py | 110 ++++++++++++++++++ 6 files changed, 518 insertions(+), 6 deletions(-) create mode 100644 tests/test_diagnostics_logs.py diff --git a/app.py b/app.py index 6958ac347..9e48bb511 100644 --- a/app.py +++ b/app.py @@ -69,10 +69,37 @@ from src.generated_images import GENERATED_IMAGE_HEADERS, resolve_generated_imag from starlette.responses import RedirectResponse # ========= LOGGING ========= -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -) +import logging.handlers +from core.constants import DATA_DIR + +_root_logger = logging.getLogger() +_root_logger.setLevel(logging.INFO) +_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +# Clear existing handlers to avoid duplicates +for _h in list(_root_logger.handlers): + _root_logger.removeHandler(_h) + +_console_h = logging.StreamHandler() +_console_h.setFormatter(_formatter) +_root_logger.addHandler(_console_h) + +try: + _log_dir = os.path.join(DATA_DIR, "logs") + os.makedirs(_log_dir, exist_ok=True) + _log_file = os.path.join(_log_dir, "app.log") + + # RotatingFileHandler is not multi-process safe (e.g. if uvicorn is run with --workers N). + # Odysseus is single-process by convention, so this is acceptable, but be aware that + # concurrent log rotation issues can arise if multiple workers are configured. + _file_h = logging.handlers.RotatingFileHandler( + _log_file, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8" + ) + _file_h.setFormatter(_formatter) + _root_logger.addHandler(_file_h) +except Exception as e: + _root_logger.warning(f"Failed to initialize file logging handler (falling back to console-only): {e}") + logger = logging.getLogger(__name__) # ========= APP ========= diff --git a/routes/diagnostics_routes.py b/routes/diagnostics_routes.py index d6763798d..e6167a80f 100644 --- a/routes/diagnostics_routes.py +++ b/routes/diagnostics_routes.py @@ -1,12 +1,13 @@ """Diagnostics routes — /api/db/stats, /api/rag/stats, /api/test/youtube, /api/test-research.""" import logging +import os from typing import Dict, Any from fastapi import APIRouter, HTTPException, Form, Request from services.youtube.youtube_handler import extract_youtube_id, extract_transcript_async -from core.constants import DEFAULT_HOST +from core.constants import DEFAULT_HOST, DATA_DIR from core.middleware import require_admin logger = logging.getLogger(__name__) @@ -28,6 +29,30 @@ def setup_diagnostics_routes( from src.service_health import collect_service_health return await collect_service_health(rag_manager, memory_vector) + @router.get("/api/diagnostics/logs") + async def get_diagnostics_logs(request: Request, limit: int = 200) -> Dict[str, Any]: + require_admin(request) + limit = max(1, min(limit, 1000)) + try: + log_file = os.path.join(DATA_DIR, "logs", "app.log") + if not os.path.exists(log_file): + return {"status": "success", "logs": []} + + # Safe tail read of the log file (max 5MB via rotation) + with open(log_file, "r", encoding="utf-8", errors="ignore") as f: + lines = f.readlines() + + tail_lines = lines[-limit:] if len(lines) > limit else lines + tail_lines = [line.rstrip('\r\n') for line in tail_lines] + + return { + "status": "success", + "logs": tail_lines + } + except Exception as e: + logger.error(f"Diagnostics logs retrieval error: {e}") + raise HTTPException(500, f"Failed to retrieve logs: {str(e)}") + @router.get("/api/db/stats") async def get_database_stats(request: Request) -> Dict[str, Any]: require_admin(request) diff --git a/static/index.html b/static/index.html index eff1f8fd7..08047f9aa 100644 --- a/static/index.html +++ b/static/index.html @@ -2232,6 +2232,61 @@