"""统一配置应用、Uvicorn 和第三方库日志""" import logging import sys from pathlib import Path from loguru import logger LOG_PATH = Path(__file__).resolve().parents[2] / "logs" / "app.log" LOG_FORMAT = "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {extra[thread_id]} | {extra[node]} | {message}" _initialized = False class InterceptHandler(logging.Handler): """将标准 logging 转发到 Loguru,兼容第三方库""" def emit(self, record: logging.LogRecord) -> None: # FastAPI 已记录的异常不再由 Uvicorn 重复打印堆栈 if record.exc_info and getattr(record.exc_info[1], "_logged_by_api", False): return try: level = logger.level(record.levelname).name except ValueError: level = record.levelno logger.opt(exception=record.exc_info).log(level, record.getMessage()) def setup_logging() -> None: """应用启动或独立测试入口调用,多次调用不会重复添加输出""" global _initialized if _initialized: return LOG_PATH.parent.mkdir(parents=True, exist_ok=True) logger.remove() logger.configure(extra={"thread_id": "-", "node": "-"}) options = { "level": "INFO", "format": LOG_FORMAT, "enqueue": True, "diagnose": False, "backtrace": False, } logger.add(sys.stderr, colorize=True, **options) logger.add( LOG_PATH, rotation="20 MB", retention="14 days", compression="zip", encoding="utf-8", **options, ) # 清理已有输出处理器,让标准日志只经过根处理器输出一次 logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO, force=True) for name in list(logging.root.manager.loggerDict): existing = logging.getLogger(name) existing.handlers.clear() existing.propagate = True _initialized = True