"""Structlog configuration for the pipeline. Call configure_structlog() once at application startup to set up structured logging with human-readable console output in dev and JSON output when use_json=True is passed. Logs always go to stderr (console/PyCharm) AND to ``logs/pipeline.log`` (rotating, 10 MB x 3 backups) so the assistant can read them without needing the IDE console. """ from __future__ import annotations import logging import sys from logging.handlers import RotatingFileHandler import structlog from common.paths import REPO_ROOT LOG_DIR = REPO_ROOT / "logs" LOG_FILE = LOG_DIR / "pipeline.log" LOG_MAX_BYTES = 10 * 1024 * 1024 LOG_BACKUP_COUNT = 3 def configure_structlog(use_json: bool) -> None: """Configure structlog processors and stdlib integration. Uses ConsoleRenderer for human-readable dev output. Integrates with stdlib logging so third-party libraries (litellm, httpx, etc.) also emit structured logs. Also attaches a RotatingFileHandler so output lands in ``logs/pipeline.log`` regardless of how the process is launched. """ shared_processors: list[structlog.types.Processor] = [ structlog.contextvars.merge_contextvars, structlog.stdlib.add_log_level, structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.UnicodeDecoder(), ] if use_json: renderer: structlog.types.Processor = structlog.processors.JSONRenderer() else: renderer = structlog.dev.ConsoleRenderer() structlog.configure( processors=[ *shared_processors, structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, renderer, ], ) console_handler = logging.StreamHandler(sys.stderr) console_handler.setFormatter(formatter) LOG_DIR.mkdir(parents=True, exist_ok=True) file_handler = RotatingFileHandler( LOG_FILE, maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUP_COUNT, encoding="utf-8", ) plain_renderer = structlog.dev.ConsoleRenderer(colors=False) file_formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, plain_renderer, ], ) file_handler.setFormatter(file_formatter) root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.addHandler(console_handler) root_logger.addHandler(file_handler) root_logger.setLevel(logging.INFO)