2026-03-18 17:18:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
loguru 统一日志配置 + InterceptHandler 拦截标准库 logging。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
from loguru import logger
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InterceptHandler(logging.Handler):
|
|
|
|
|
|
"""Route standard-library logging messages into loguru."""
|
|
|
|
|
|
|
|
|
|
|
|
def emit(self, record: logging.LogRecord) -> None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
level = logger.level(record.levelname).name
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
level = record.levelno
|
|
|
|
|
|
|
|
|
|
|
|
frame, depth = logging.currentframe(), 2
|
|
|
|
|
|
while frame and frame.f_code.co_filename == logging.__file__:
|
|
|
|
|
|
frame = frame.f_back
|
|
|
|
|
|
depth += 1
|
|
|
|
|
|
|
2026-03-19 14:36:14 +08:00
|
|
|
|
logger.opt(depth=depth, exception=record.exc_info).log(
|
|
|
|
|
|
level, record.getMessage()
|
|
|
|
|
|
)
|
2026-03-18 17:18:23 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_logging() -> None:
|
2026-03-20 15:15:35 +08:00
|
|
|
|
"""Call once at process entry (API:`main`;Worker:`celery_app` 在首行调用)。
|
|
|
|
|
|
|
|
|
|
|
|
Celery 需在 `app.tasks.celery_app` 中设置 `worker_hijack_root_logger=False`,否则
|
|
|
|
|
|
会覆盖根 logger,无法与下方 InterceptHandler + loguru 格式对齐。
|
|
|
|
|
|
"""
|
2026-03-18 17:18:23 +08:00
|
|
|
|
logger.remove()
|
|
|
|
|
|
|
|
|
|
|
|
logger.add(
|
|
|
|
|
|
sys.stderr,
|
|
|
|
|
|
level="INFO",
|
|
|
|
|
|
format=(
|
|
|
|
|
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
2026-03-20 15:15:35 +08:00
|
|
|
|
"<level>{level.name: <8}</level> | "
|
2026-03-18 17:18:23 +08:00
|
|
|
|
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
|
|
|
|
|
|
"{extra[request_id]} | "
|
|
|
|
|
|
"<level>{message}</level>"
|
|
|
|
|
|
),
|
|
|
|
|
|
backtrace=True,
|
|
|
|
|
|
diagnose=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 将根 logger 重定向到 loguru,不再使用 basicConfig(文档要求:统一走 loguru)
|
|
|
|
|
|
root = logging.getLogger()
|
|
|
|
|
|
root.handlers = [InterceptHandler()]
|
|
|
|
|
|
root.setLevel(0)
|
|
|
|
|
|
|
|
|
|
|
|
for name in (
|
|
|
|
|
|
"uvicorn",
|
|
|
|
|
|
"uvicorn.error",
|
|
|
|
|
|
"uvicorn.access",
|
|
|
|
|
|
"sqlalchemy.engine",
|
|
|
|
|
|
"celery",
|
|
|
|
|
|
"celery.worker",
|
|
|
|
|
|
):
|
|
|
|
|
|
logging.getLogger(name).handlers = [InterceptHandler()]
|
|
|
|
|
|
|
|
|
|
|
|
logger.configure(extra={"request_id": "-"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
|
|
|
|
"""获取具名 logger,统一走 loguru 拦截。各模块应使用此函数而非直接 import logging。"""
|
|
|
|
|
|
return logging.getLogger(name)
|