feat: 引入 Loguru
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user