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 响应
- 增加
task_conflict业务异常和 409 映射。 - 给日志增加 user_id,但确保未认证请求仍可记录。
- 测试未传请求 ID 时响应获得非空值。
- 故意引发未处理异常,设计一个安全的 500 错误处理器。
- 列出必须脱敏的五类请求字段。
最后,用测试固定这次改动
手动请求通过后,运行与本文对应的自动化测试:
python -m pytest tests/test_ch14.py -q
python tests/validate_course.py
写在最后
服务已经可诊断。下一篇使用 Docker 固化 Python、依赖、启动命令和监听地址,验证同一镜像能在不同机器提供一致服务。