12 · 错误处理、日志与配置
后端不只是"能返回正确结果",还要优雅地返回错误、完整地留下痕迹、灵活地切换配置。前端在这三块最容易轻敌。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 三个"生产级"基本功:统一错误体、结构化日志、分层配置 |
| 核心内容 | 错误体设计、异常处理机制、日志分级与结构、配置分层与密钥管理 |
| 读完能回答 | 后端错误码怎么设计?日志怎么打才能查问题?配置怎么管理才能本地/测试/生产切换? |
| 相关板块 | C02 · API 设计 | C08 · 可观测性 | C09 · 服务端语言上手 |
一、统一错误体
前端最烦"接口返回一堆不同格式的报错"。后端要从第一天就统一:
json
{
"error": {
"code": "ITEM_NOT_FOUND",
"message": "商品不存在",
"details": { "item_id": "42" }
}
}| 字段 | 用途 | 约定 |
|---|---|---|
code | 机器可读的错误码 | 大写下划线,前端据此分支 |
message | 人可读的提示 | 面向最终用户 |
details | 调试用细节 | 不要泄露敏感信息 |
错误码 vs 状态码:HTTP 状态码粗(404/400/500),错误码细(
ITEM_NOT_FOUND)。前端判断逻辑用错误码,HTTP 码给网关和监控用。
二、异常处理机制
python
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class BusinessError(Exception):
def __init__(self, code: str, message: str, status: int = 400):
self.code = code
self.message = message
self.status = status
@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
return JSONResponse(
status_code=exc.status,
content={"error": {"code": exc.code, "message": exc.message}},
)套路:业务代码只抛领域异常,由统一 handler 转成错误体。业务层不碰 HTTP,错误格式集中管理。
2.1 意外异常的兜底
python
@app.exception_handler(Exception)
async def unhandled(request: Request, exc: Exception):
# 记录完整堆栈到日志,返回给客户端一个不泄露细节的 500
return JSONResponse(status_code=500, content={"error": {"code": "INTERNAL"}})原则:客户端永远不要看到堆栈和内部细节(信息泄露),但服务端必须完整记录堆栈用于排查。
三、日志:结构化的才可查
3.1 分级
| 级别 | 何时用 | 例子 |
|---|---|---|
DEBUG | 开发调试,生产默认关 | 中间变量 |
INFO | 关键流程 | 用户登录、下单成功 |
WARNING | 可恢复的异常 | 重试成功、限流触发 |
ERROR | 操作失败 | 数据库连不上 |
CRITICAL | 服务不可用 | 启动失败 |
3.2 用 logging 而非 print
python
import logging
logger = logging.getLogger("my_api")
def create_order(...):
logger.info("order created", extra={"order_id": oid, "user_id": uid})为什么不用
logging有级别、能进日志系统、能按环境配置;
3.3 结构化日志
生产里推荐 JSON 日志(logger.info(..., extra={"order_id": oid}) 配合 JSON formatter),这样可被日志平台按字段检索:
json
{"ts": "...", "level": "INFO", "logger": "my_api", "msg": "order created", "order_id": "42"}与可观测性(链路追踪、指标)的关系见 C08。
四、配置分层
| 层 | 来源 | 优先级 |
|---|---|---|
| 默认值 | 代码里 Settings 的默认 | 最低 |
| 本地覆盖 | .env / config.local | 中 |
| 环境注入 | 部署平台的环境变量 | 高 |
| 密钥管理 | 密钥服务(如云厂商 Secret Manager) | 最高 |
python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
env: str = "dev"
database_url: str
redis_url: str = "redis://localhost:6379/0"
secret_key: str = ""
model_config = {"env_file": ".env", "extra": "ignore"}密钥管理原则:代码仓库里永不提交密钥。本地用
.env(加入.gitignore),生产用环境变量或密钥服务。
自检清单
- [ ] 能设计统一错误体(code/message/details),并说清与 HTTP 状态码的分工
- [ ] 会用异常 handler 集中处理业务错误和兜底 500
- [ ] 会正确使用日志分级,用
logging而不用print - [ ] 会按"默认值 → 本地 → 环境 → 密钥服务"分层管理配置
- [ ] 确认密钥没有提交进 git
常见误区
- 误区:错误码就是 HTTP 状态码。 状态码粗、错误码细,两个都要有,前端按错误码分支。
- 误区:把堆栈返回给客户端。 这是信息泄露;完整堆栈只进服务端日志。
- 误区:
print够用。 没有级别、没有结构、进不了日志系统,排查会非常痛苦。 - 误区:配置写在代码里。 环境差异(本地/测试/生产)靠配置切换,硬编码一换环境就炸。
动手练习
练习 A:给 Todo API 加统一错误体
给 C09 的 Todo API 增加 ITEM_NOT_FOUND 错误码和兜底 handler,前端能根据错误码分支。
练习 B:结构化日志
给下单流程打结构化 INFO 日志(含 order_id/user_id),配 JSON formatter,在日志里检索一个订单。
练习 C:三层配置
用 BaseSettings 实现 dev/test/prod 三套配置,并把密钥全部移出代码。
相关文档
本区
- 02 · API 设计 —— 错误体与接口约定
- 08 · 可观测性 —— 日志/指标/链路的关系
- 09 · 服务端语言上手 —— 配置读法的起点
- 11 · 后端测试实战 —— 错误体是可测契约
其他域
- 12 · 前端可观测性与线上排障 —— 前端监控思维对照