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