init commit
This commit is contained in:
89
app/utils/logging.py
Normal file
89
app/utils/logging.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""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 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)
|
||||
Reference in New Issue
Block a user