import logging import sys from loguru import logger from pydantic import BaseModel class LogConfig(BaseModel): """Logging configuration""" LOGGER_NAME: str = "manga_inventory_api" LOG_FORMAT: str = ( "{time:YYYY-MM-DD HH:mm:ss.SSS} | " "{level: <8} | " "{name}:{function}:{line} - " "{message}" ) LOG_LEVEL: str = "INFO" LOG_FILE_PATH: str | None = None LOG_ROTATION: str | None = "20 MB" LOG_RETENTION: str | None = "1 week" LOG_JSON: bool = False def setup_logging(config: LogConfig = LogConfig()) -> None: """Configure Loguru logger""" # Intercept standard logging logging.getLogger().handlers = [InterceptHandler()] # Remove default handlers logger.remove() # Add console handler logger.add( sys.stdout, enqueue=True, backtrace=True, format=config.LOG_FORMAT, level=config.LOG_LEVEL, colorize=True, ) # Add file handler if path is specified if config.LOG_FILE_PATH: logger.add( config.LOG_FILE_PATH, rotation=config.LOG_ROTATION, retention=config.LOG_RETENTION, enqueue=True, backtrace=True, format=config.LOG_FORMAT, level=config.LOG_LEVEL, colorize=False, serialize=config.LOG_JSON, ) # Configure standard library logging for _log in ["uvicorn", "uvicorn.error", "fastapi"]: _logger = logging.getLogger(_log) _logger.handlers = [InterceptHandler()] _logger.propagate = False logger.info("Logging configured successfully") class InterceptHandler(logging.Handler): """ Intercept handler for standard library logging to redirect to loguru. """ def emit(self, record: logging.LogRecord) -> None: """ Intercepts log records and redirects them to loguru. """ try: level = logger.level(record.levelname).name except ValueError: level = record.levelno frame, depth = logging.currentframe(), 2 while frame.f_code.co_filename == logging.__file__: frame = frame.f_back depth += 1 logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())