83 lines
2.4 KiB
Python
83 lines
2.4 KiB
Python
|
|
"""将 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=(
|
|||
|
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
|||
|
|
"<level>{level: <8}</level> | "
|
|||
|
|
"<level>{message}</level>"
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
_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
|