From 6c9a16a7a82d58d22501b23cb08e9c758f0df773 Mon Sep 17 00:00:00 2001 From: Giuseppe Castelluccio Date: Sun, 7 Jun 2026 19:26:22 +0200 Subject: [PATCH] fix: search analytics FileHandler crashes on startup writing to read-only image layer (#2366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: move search analytics log to writable /app/logs volume services/search/analytics.py opened a FileHandler at module import time pointing to /app/services/search_engine_error.log — inside the container image's read-only layer. The process runs as non-root so the open() fails with PermissionError, crashing uvicorn before it ever binds. ANALYTICS_FILE had the same problem. Both paths now point to /app/logs (bind-mounted from the host data directory). The FileHandler creation is wrapped in try/except so a missing mount doesn't hard-crash on import. Co-Authored-By: Claude Sonnet 4.6 * fix: derive log dir from DATA_DIR instead of hardcoded /app/logs Fixes reviewer feedback on #2366: /app/logs only exists inside Docker, so native runs couldn't write the analytics file. DATA_DIR resolves to the repo's data/ directory on native and /app/data (writable mount) in Docker, making both the error log handler and ANALYTICS_FILE work on every platform. --------- Co-authored-by: Claude Sonnet 4.6 --- services/search/analytics.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/services/search/analytics.py b/services/search/analytics.py index 64e61e962..b5602bae4 100644 --- a/services/search/analytics.py +++ b/services/search/analytics.py @@ -6,21 +6,29 @@ from collections import Counter from pathlib import Path from typing import Dict, Any +from core.constants import DATA_DIR + from .cache import cache_metrics logger = logging.getLogger(__name__) -# Dedicated error logger with file handler -_error_log_path = Path(__file__).resolve().parent.parent / "search_engine_error.log" -_error_handler = logging.FileHandler(_error_log_path, encoding="utf-8") -_error_handler.setLevel(logging.WARNING) -_error_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")) +# Dedicated error logger — write to the data logs directory (writable on both +# native runs and Docker, where DATA_DIR resolves to the bind-mounted volume). +_log_dir = Path(DATA_DIR) / "logs" +_error_log_path = _log_dir / "search_engine_error.log" error_logger = logging.getLogger("search_engine_error") -error_logger.addHandler(_error_handler) error_logger.propagate = False +try: + _log_dir.mkdir(parents=True, exist_ok=True) + _error_handler = logging.FileHandler(_error_log_path, encoding="utf-8") + _error_handler.setLevel(logging.WARNING) + _error_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")) + error_logger.addHandler(_error_handler) +except Exception as _e: + logging.getLogger(__name__).warning("search_engine_error log handler unavailable: %s", _e) -# Analytics file -ANALYTICS_FILE = Path(__file__).resolve().parent.parent / "search_analytics.json" +# Analytics file — also in the writable logs volume. +ANALYTICS_FILE = _log_dir / "search_analytics.json" # ----------------------------------------------------------------------