feat: 引入 Loguru

This commit is contained in:
2026-09-29 00:47:57 +08:00
parent 0fdde8c3e6
commit 437fcd74b1
8 changed files with 139 additions and 31 deletions
+7 -4
View File
@@ -1,19 +1,17 @@
"""业务异常与全局 API 异常处理"""
import logging
from http import HTTPStatus
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from loguru import logger
from openai import APIError, APITimeoutError
from starlette.exceptions import HTTPException
from app.common.response import error_content
from app.exceptions import AppError, DecisionError, ImportProcessError
logger = logging.getLogger(__name__)
class BizException(AppError):
"""可安全返回给调用方的业务错误"""
@@ -54,7 +52,12 @@ async def handle_unexpected_exception(
_request: Request, exc: Exception
) -> JSONResponse:
# 详细异常仅记录在服务端,不将堆栈或内部错误内容返回给前端
logger.error("未处理的服务器异常", exc_info=(type(exc), exc, exc.__traceback__))
logger.bind(thread_id=getattr(exc, "_agent_thread_id", "-")).opt(
exception=exc
).error("未处理的服务器异常")
exc._logged_by_api = True
# 节点包装异常时沿原始原因查找,区分上游服务故障与内部错误
cause = exc
while isinstance(cause, ImportProcessError):
+62
View File
@@ -0,0 +1,62 @@
"""统一配置应用、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