开篇:一个让人抓狂的下午
“接口挂了,返回 500。”
看到这条消息,你打开服务器日志,心想:来吧,看看是什么妖魔鬼怪。
2024-01-15 14:30:00 | INFO | 应用启动完成 2024-01-15 14:30:05 | INFO | 收到请求: POST /api/generate然后……就没了。
没有错误信息,没有堆栈跟踪,什么都没有。
Bug 像个忍者,来无影去无踪。
你开始加 print,部署,再加 print,再部署……三小时后,终于发现是某个变量是 None。
三个小时,就为了找一个空指针。
罪魁祸首?翻开代码,你看到了这行:
try:result=some_function()exceptException:pass# 就是这行!某位前人写的代码,捕获了所有异常,然后——什么都不做。
异常就这么被"吞"掉了,悄无声息,像被灭霸打了个响指。
如果你也经历过这种绝望,这篇文章就是为你准备的。
异常处理的"三道防线"
好的异常处理就像洋葱——一层一层的(而且可能让你流泪)。
三道防线,各司其职:
| 防线 | 职责 | 比喻 |
|---|---|---|
| 全局异常处理器 | 兜底所有漏网之鱼 | 最后一道城墙 |
| 业务异常处理 | 处理"意料之中"的错误 | 前线哨兵 |
| 端点级 try-except | 精细化异常恢复 | 贴身保镖 |
记住:异常不会消失,只会被藏起来。我们的目标是:让每个异常都无处可藏。
第一步:给异常办个"身份证"
裸奔的raise Exception("出错了")是不够的。我们需要给异常分门别类:
# app/core/exceptions.pyfromtypingimportOptional,AnyclassAppException(Exception):"""应用异常基类 —— 所有业务异常的祖宗"""def__init__(self,message:str,code:str="UNKNOWN_ERROR",status_code:int=500,details:Optional[Any]=None):self.message=message self.code=code# 错误码,前端靠这个判断self.status_code=status_code# HTTP 状态码self.details=details# 额外信息,想塞啥塞啥super().__init__(message)classValidationError(AppException):"""参数不对?400 伺候"""def__init__(self,message:str,details:Any=None):super().__init__(message,"VALIDATION_ERROR",400,details)classNotFoundError(AppException):"""找不到?404 安排"""def__init__(self,resource:str,resource_id:str):super().__init__(f"{resource}not found:{resource_id}","NOT_FOUND",404,{"resource":resource,"id":resource_id})classGenerationError(AppException):"""AI 罢工了"""def__init__(self,message:str,model:str=None):super().__init__(message,"GENERATION_ERROR",500,{"model":model})classExternalServiceError(AppException):"""第三方服务挂了,锅不在我"""def__init__(self,service:str,message:str):super().__init__(f"{service}error:{message}","EXTERNAL_SERVICE_ERROR",502,# 502 = 上游挂了{"service":service})有了这套"身份证"系统,每个异常都有:
- 错误码:前端可以根据 code 显示不同的提示
- 状态码:HTTP 语义正确,运维监控不会瞎报警
- 详情:调试时的救命稻草
第二步:设立"全局关卡"
接下来,在 FastAPI 里注册异常处理器。把它想象成机场安检——每个异常都得过这一道:
# app/main.pyfromfastapiimportFastAPI,Requestfromfastapi.responsesimportJSONResponsefromapp.core.exceptionsimportAppExceptionfromapp.core.loggerimportloggerimporttraceback app=FastAPI()@app.exception_handler(AppException)asyncdefapp_exception_handler(request:Request,exc:AppException):"""业务异常处理器 —— 处理"可预期的坏消息" """logger.warning(f"业务异常 [{exc.code}]:{exc.message}",extra={"path":request.url.path,"method":request.method,"code":exc.code,"details":exc.details})returnJSONResponse(status_code=exc.status_code,content={"success":False,"error":{"code":exc.code,"message":exc.message,"details":exc.details}})@app.exception_handler(Exception)asyncdefglobal_exception_handler(request:Request,exc:Exception):"""全局异常处理器 —— 最后一道城墙,专治各种"意外惊喜" """error_detail={"code":"INTERNAL_ERROR","message":"An internal error occurred. Please try again later."}# 开发环境?把底裤都给你看ifsettings.DEBUG:error_detail["message"]=str(exc)error_detail["type"]=type(exc).__name__ error_detail["traceback"]=traceback.format_exc().split("\n")# 不管什么环境,日志里必须有完整信息logger.error(f"未处理的异常:{type(exc).__name__}:{str(exc)}",extra={"path":request.url.path,"method":request.method,"traceback":traceback.format_exc()# 完整堆栈,一个字都不能少!})returnJSONResponse(status_code=500,content={"success":False,"error":error_detail})划重点:
AppException用WARNING级别——这是"意料之中"的错误- 未知
Exception用ERROR级别——这是"意料之外"的惊喜 traceback.format_exc()是你的好朋友,完整堆栈一览无余- 生产环境别暴露内部错误,不然黑客会感谢你的坦诚
第三步:端点级"精细作战"
全局处理器是最后防线,但有些异常需要在端点级别就地解决:
# app/api/endpoints/generate.py@router.post("/shot-image")asyncdefgenerate_shot_image(request:GenerateShotImageRequest):"""生成分镜图片 —— 一个充满意外的端点"""# 参数校验:先礼后兵ifnotrequest.prompt.strip():raiseValidationError("Prompt cannot be empty")try:adapter=ImageGenerationAdapterFactory.get_current_adapter()result=awaitadapter.generate_shot_image(prompt=request.prompt,width=request.width,height=request.height)ifnotresult.get("success"):raiseGenerationError(result.get("error","Unknown generation error"),model=settings.IMAGE_MODEL)return{"success":True,"data":result}exceptGenerationError:raise# 已经是业务异常,放它走excepttorch.cuda.OutOfMemoryError:# 显存爆了?给个人话提示raiseGenerationError("GPU out of memory. Please try a smaller image size.")exceptExceptionase:# 未知异常:先记录,再转换logger.error(f"Unexpected error in generate_shot_image:{e}")raiseGenerationError(f"Generation failed:{str(e)}")异常处理策略表:
| 情况 | 处理方式 | 理由 |
|---|---|---|
| 已是业务异常 | 直接raise | 已经有身份证了 |
| 已知特定异常 | 转换为业务异常 | 给它办个身份证 |
| 未知异常 | 记日志 + 转换 | 先留案底,再处理 |
第四步:日志配置——"案发现场"的监控探头
推荐 loguru,比标准库的 logging 好用一万倍(真的):
# app/core/logger.pyfromloguruimportloggerimportsys logger.remove()# 先清场# 控制台输出:花里胡哨但好用logger.add(sys.stdout,format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | ""<level>{level: <8}</level> | ""<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | ""<level>{message}</level>",level="DEBUG")# 文件日志:朴实无华但可靠logger.add("logs/app_{time:YYYY-MM-DD}.log",format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}",level="INFO",rotation="00:00",# 每天零点轮转retention="30 days",# 保留 30 天compression="zip"# 自动压缩,省空间)# 错误日志:单独伺候,VIP 待遇logger.add("logs/error_{time:YYYY-MM-DD}.log",format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}\n{exception}",level="ERROR",rotation="00:00",retention="90 days"# 错误日志多留几天,翻旧账用得上)第五步:请求追踪——给每个请求一个"案件编号"
importtimeimportuuid@app.middleware("http")asyncdeflog_requests(request:Request,call_next):"""请求日志中间件 —— 来了就得登记"""request_id=str(uuid.uuid4())[:8]# 8 位够用了logger.info(f"[{request_id}] -->{request.method}{request.url.path}")start=time.time()response=awaitcall_next(request)duration=time.time()-start logger.info(f"[{request_id}] <--{response.status_code}in{duration:.2f}s")# 响应头里也带上,方便前端response.headers["X-Request-ID"]=request_idreturnresponse效果展示:
2024-01-15 14:30:00 | INFO | [a1b2c3d4] --> POST /api/generate/shot-image 2024-01-15 14:30:12 | INFO | [a1b2c3d4] <-- 200 in 12.34s前端说"接口报错了",你只需要问一句:“Request ID 多少?”
然后grep a1b2c3d4 logs/error_*.log,破案。
红线:绝对禁止的写法
立个规矩,刻在 DNA 里:
# 禁止!禁止!禁止!try:something()exceptException:pass# 这是在犯罪这种代码的危害:
- 异常凭空消失,debug 时怀疑人生
- 埋下定时炸弹,不知道什么时候爆炸
- 让接手的同事想打人
正确姿势:
# 姿势一:只捕获特定异常try:result=maybe_fail()exceptFileNotFoundError:result=default_value# 这个异常我能处理exceptPermissionErrorase:logger.warning(f"权限不足:{e}")raise# 这个我处理不了,抛出去# 姿势二:实在要忽略,至少留个遗言try:optional_operation()exceptSomeSpecificErrorase:logger.debug(f"忽略的异常(有意为之):{e}")快速抄作业清单
| 原则 | 做法 |
|---|---|
| 不吞异常 | except: pass写一次,绩效扣一分 |
| 分层处理 | 全局 → 业务 → 端点,层层设防 |
| 日志完整 | 堆栈、请求 ID、路径,一个都不能少 |
| 错误码标准化 | 定义业务异常类,别裸抛 Exception |
| 环境区分 | 开发给详情,生产给脸色 |
| 请求可追踪 | X-Request-ID,追凶利器 |
总结:异常处理三原则
把这三句话贴在工位上:
- 要么处理它——你知道怎么应对这个异常
- 要么记录它——你不知道怎么处理,但要留下证据
- 要么抛出它——让更上层的人来处理
但绝不能忽视它。
有了这套体系,下次接口报 500,你只需要:
- 拿到 Request ID
- 搜索错误日志
- 看完整堆栈
- 定位问题
整个过程,一分钟。
再也不用三小时排查一个空指针了。