FastAPI 出错后怎么快速定位?统一错误体与请求日志

摘要:为每个请求生成可传播的 request ID,让业务错误和参数错误共享稳定响应结构,并在服务端日志记录方法、路径、状态码和耗时。

请求标识关联客户端错误与服务端日志

图:同一个请求标识贯穿响应和日志,把客户端遇到的错误精确关联到服务端事件。

接口出现问题时,调用者通常只能提供“某个时间点请求失败了”。如果响应没有可关联标识,服务端日志又只有散乱文本,排查就会变成在大量记录里猜哪一条属于他。

解决思路并不复杂:给请求一张贯穿链路的身份证,把它同时放进响应和日志;业务异常只表达业务事实,HTTP 状态码与错误 JSON 由统一处理器生成。

先看一次 404 如何关联到日志

fastapi dev examples/ch14_logging/main.py --log-level info
curl -i -H "X-Request-ID: req-demo" http://127.0.0.1:8000/tasks/999

预期 404 响应包含:

{"error":{"code":"task_not_found","message":"任务不存在","request_id":"req-demo","task_id":999}}

服务器终端日志能搜索到同一个 req-demo

先决定哪些信息留给客户端,哪些只进日志

cd 04-fastapi-beginner
source .venv/bin/activate

打开 日志示例。本文使用标准 logging;生产可以输出 JSON 并交给 OpenTelemetry、日志平台或云服务采集。

让业务异常、错误体和日志各司其职

定义业务异常

class TaskNotFoundError(Exception):
    def __init__(self, task_id: int) -> None:
        self.task_id = task_id

业务函数表达“任务不存在”,HTTP 映射集中在异常处理器中。

注册统一处理器

@app.exception_handler(TaskNotFoundError)
async def task_not_found_handler(request, error):
    return JSONResponse(
        status_code=404,
        content=error_body(request, "task_not_found", "任务不存在"),
    )

校验错误处理器沿用同样的 error.code/message/request_id 外壳,并把字段详情放入 details。

记录完成日志

logger.info(
    "request_complete method=%s path=%s status=%s duration_ms=%.2f request_id=%s",
    request.method,
    request.url.path,
    response.status_code,
    duration_ms,
    request.state.request_id,
)

参数化 logging 避免在日志等级关闭时提前拼接字符串。

业务异常、统一错误体和结构化日志分层处理

图:业务层表达异常,处理器生成安全错误体,日志系统记录可检索的内部信息。

用同一个 request ID 请求 200、404 和 422

curl -i -H "X-Request-ID: req-ok" http://127.0.0.1:8000/tasks/1
curl -i -H "X-Request-ID: req-missing" http://127.0.0.1:8000/tasks/999
curl -i -H "X-Request-ID: req-invalid" http://127.0.0.1:8000/tasks/abc
python -m pytest tests/test_ch14.py -q

预期三次请求分别为 200、404、422;响应和对应完成日志使用相同 ID。

稳定错误码给机器,request ID 留给排查

错误码 task_not_found 面向机器且保持稳定,中文 message 面向人并可迭代。客户端不应通过解析中文文案判断业务分支。

request ID 应从网关向下游服务传播,而不是每层都生成新值。它不是认证信息,不能代替用户 ID、trace ID 或权限校验。

客户端错误通常记录 INFO/WARNING,未预期服务错误记录 ERROR 并保留服务端堆栈。响应只返回安全摘要和 request ID,详细异常留在受控日志。

同一请求标识同时出现在客户端响应和服务端日志

图:同一请求标识贯穿响应和日志,排查时可以把两端事件准确对应起来。

可观测性也可能制造新的敏感数据

  • 日志打印密码、Token 或完整请求体:会形成新的敏感数据源。
  • 所有异常都改成 500:调用者无法区分输入、权限和服务故障。
  • 错误体结构因路由不同而变化:客户端处理成本急剧上升。
  • 每层生成不同 request ID:失去关联价值。
  • 捕获异常但不记录:客户端得到 500,服务端没有诊断证据。

把未处理异常也收进安全的 500 响应

  1. 增加 task_conflict 业务异常和 409 映射。
  2. 给日志增加 user_id,但确保未认证请求仍可记录。
  3. 测试未传请求 ID 时响应获得非空值。
  4. 故意引发未处理异常,设计一个安全的 500 错误处理器。
  5. 列出必须脱敏的五类请求字段。

最后,用测试固定这次改动

手动请求通过后,运行与本文对应的自动化测试:

python -m pytest tests/test_ch14.py -q
python tests/validate_course.py

写在最后

服务已经可诊断。下一篇使用 Docker 固化 Python、依赖、启动命令和监听地址,验证同一镜像能在不同机器提供一致服务。