"""Centralized logging setup: structlog for file logs, Rich for terminal output.""" import logging from pathlib import Path import structlog from rich.console import Console # Shared Rich console instance for the entire application console = Console() def setup_logging( log_file: Path | None = None, log_level: str = "INFO", verbose: bool = False, ) -> None: """Configure dual logging: Rich handler for terminal, optional file handler. Args: log_file: Optional path to a log file for structured file logging. log_level: Minimum log level (DEBUG, INFO, WARNING, ERROR). verbose: If True, sets log level to DEBUG regardless of log_level. """ effective_level = "DEBUG" if verbose else log_level.upper() console_level = effective_level if verbose else "WARNING" # Configure stdlib root logger root_logger = logging.getLogger() root_logger.setLevel(effective_level) # Clear any existing handlers to avoid duplicates on re-init root_logger.handlers.clear() # Rich handler for terminal output — only warnings+ unless verbose from rich.logging import RichHandler rich_handler = RichHandler( console=console, show_time=True, show_path=verbose, markup=True, rich_tracebacks=True, ) rich_handler.setLevel(console_level) root_logger.addHandler(rich_handler) # Optional file handler for persistent structured logs if log_file is not None: log_file.parent.mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler(str(log_file), encoding="utf-8") file_handler.setLevel(effective_level) file_formatter = logging.Formatter( "%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) file_handler.setFormatter(file_formatter) root_logger.addHandler(file_handler) # Configure structlog to route through stdlib logging structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) def setup_logging_for_tui() -> None: """Reconfigure logging for Textual TUI mode. Removes the RichHandler (which writes to stdout and corrupts the TUI) while preserving any file handlers. Call this from the Textual App's on_mount() before any agent work begins. """ from rich.logging import RichHandler root_logger = logging.getLogger() root_logger.handlers = [ h for h in root_logger.handlers if not isinstance(h, RichHandler) ] def get_logger(name: str) -> structlog.stdlib.BoundLogger: """Get a named structlog logger. Args: name: Logger name, typically __name__ of the calling module. Returns: A bound structlog logger instance. """ return structlog.get_logger(name)