Files
SneakyCode/app/utils/logging.py
2026-03-11 12:41:03 -05:00

105 lines
3.4 KiB
Python

"""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)