import os
import sys
import logging
from loguru import logger
class InterceptHandler(logging.Handler):
"""
Standard logging handler to intercept standard logging messages
and redirect them to Loguru.
"""
def emit(self, record: logging.LogRecord) -> None:
# Get corresponding Loguru level if it exists
try:
level: str | int = logger.level(record.levelname).name
except ValueError:
level = record.levelno
# Find caller from where originated the logged message
frame = logging.currentframe()
depth = 2
while frame and 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()
)
def setup_logging():
"""
Configures Loguru for console and file logging, and intercepts
standard library logging (uvicorn, httpx, etc.).
"""
os.makedirs("logs", exist_ok=True)
# Remove default handlers
logger.remove()
# Log format specifications
console_format = (
"{time:YYYY-MM-DD HH:mm:ss} | "
"{level: <8} | "
"{name}:{function}:{line} - "
"{message}"
)
file_format = "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}"
# Console Handler (colored)
logger.add(
sys.stdout,
level="INFO",
format=console_format,
colorize=True,
enqueue=True,
backtrace=True,
diagnose=True,
)
# Rotating File Handler (5 MB per file, max 3 backups)
logger.add(
"logs/app.log",
level="INFO",
format=file_format,
rotation="5 MB",
retention=3,
encoding="utf-8",
enqueue=True,
)
# Intercept standard Python logging
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
# Ensure uvicorn, fastapi, watchfiles, and httpx use the intercept handler
for log_name in ("uvicorn", "uvicorn.access", "uvicorn.error", "fastapi", "watchfiles", "httpx"):
mod_logger = logging.getLogger(log_name)
mod_logger.handlers = [InterceptHandler()]
if "watchfiles" in log_name:
mod_logger.setLevel(logging.WARNING)
return logger
__all__ = ["logger", "setup_logging", "InterceptHandler"]