2026-03-18 17:18:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
loguru 统一日志配置 + InterceptHandler 拦截标准库 logging。
|
2026-03-22 16:45:57 +08:00
|
|
|
|
|
|
|
|
|
|
日志约定:
|
|
|
|
|
|
- INFO:面向运维的稳定摘要,避免敏感字段与高频噪音。
|
|
|
|
|
|
- DEBUG:可记录完整上下文、用户内容、连接串、URL 等敏感信息;仅用于受控环境排查,
|
|
|
|
|
|
生产环境勿长期开启 DEBUG。
|
|
|
|
|
|
|
|
|
|
|
|
由 ``Settings.log_level`` 控制(环境变量 ``LOG_LEVEL``,默认 ``INFO``);
|
|
|
|
|
|
设为 ``DEBUG`` 时上述详细日志才会输出。
|
2026-03-18 17:18:23 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
from loguru import logger
|
|
|
|
|
|
|
2026-03-22 16:45:57 +08:00
|
|
|
|
from app.core.config import settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sink_min_level() -> str:
|
|
|
|
|
|
raw = (settings.log_level or "INFO").strip().upper()
|
|
|
|
|
|
if raw in ("TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL"):
|
|
|
|
|
|
return raw
|
|
|
|
|
|
return "INFO"
|
|
|
|
|
|
|
2026-03-18 17:18:23 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-03-22 16:45:57 +08:00
|
|
|
|
level=_sink_min_level(),
|
2026-03-18 17:18:23 +08:00
|
|
|
|
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)
|