"""将 loguru 与标准 logging 桥接,使 uvicorn / FastAPI / Starlette 日志走同一套格式。""" from __future__ import annotations import json import logging import os import sys from typing import Any from loguru import logger _loguru_sink_configured = False def format_json_pretty( data: Any, *, indent: int = 2, max_chars: int | None = 24_000, ) -> str: """将对象格式化为带缩进、保留中文的 JSON 字符串,供 loguru 多行输出。 ``max_chars`` 用于避免单条日志过大;超长时截断并标注。 """ try: s = json.dumps(data, ensure_ascii=False, indent=indent, default=str) except (TypeError, ValueError): return repr(data) if max_chars is not None and len(s) > max_chars: return s[:max_chars] + "\n... (truncated, max_chars=" + str(max_chars) + ")" return s class InterceptHandler(logging.Handler): """将标准 logging 记录转发到 loguru。""" def emit(self, record: logging.LogRecord) -> None: try: level: str | int = logger.level(record.levelname).name except ValueError: level = record.levelno logger.opt(depth=6, exception=record.exc_info).log(level, record.getMessage()) def setup_logging() -> None: """配置 loguru sink,并把 uvicorn / fastapi 等 logger 接到 loguru。 可在模块 import 时与 lifespan 启动时各调用一次;后者用于覆盖 uvicorn 启动后写入的 handler。 """ global _loguru_sink_configured level = os.environ.get("LOG_LEVEL", "INFO").upper() if not _loguru_sink_configured: logger.remove() logger.add( sys.stderr, level=level, colorize=sys.stderr.isatty(), format=( "{time:YYYY-MM-DD HH:mm:ss.SSS} | " "{level: <8} | " "{message}" ), ) _loguru_sink_configured = True intercept = InterceptHandler() logging.root.handlers = [intercept] logging.root.setLevel(logging.DEBUG) for name in ( "uvicorn", "uvicorn.error", "uvicorn.access", "uvicorn.asgi", "fastapi", "starlette", "starlette.requests", ): lg = logging.getLogger(name) lg.handlers.clear() lg.propagate = True