接口报 500 了,日志里却空空的?如何给你的 AI 后端加上「三层防护」

在这里插入图片描述

阅读大约需 10 分钟


那个神秘的 500 错误

周三下午,前端同事发来消息:“小禾,生成图片的接口挂了,返回 500。”

小禾打开服务器日志:

2024-01-15 14:30:00 | INFO | 应用启动完成
2024-01-15 14:30:05 | INFO | 收到请求: POST /api/generate
...

然后就没了。

没有错误信息,没有堆栈跟踪,什么都没有。

小禾本地跑了一遍,正常。

他又看了看前端控制台:

POST /api/generate 500 (Internal Server Error)
{"detail": "Internal Server Error"}

就这么一行,没有任何有用的信息。

小禾开始漫长的排查之旅:

14:35 - 加了几个 print,重新部署
14:50 - 发现 print 没输出,可能没走到那
15:20 - 又加了几个 print,又部署
15:45 - 终于发现是某个深层函数抛了异常
16:00 - 定位到问题:一个变量是 None
16:10 - 修复,部署,测试通过

三个小时,就为了找一个空指针。

小禾越想越气:为什么异常没打日志?

他翻了翻代码,找到了罪魁祸首:

try:
    result = some_function()
except Exception:
    pass  # 就是这行!

某位前人写的代码,捕获了所有异常,然后什么都不做。

异常就这么被吞掉了,悄无声息。


异常处理的三个层级

痛定思痛,小禾决定重构整个异常处理体系。

他画了张架构图:

抛出异常
业务异常
未知异常
HTTP 请求
Layer 1: 全局异常处理器
Layer 2: 业务异常处理
Layer 3: 端点级 try-except
业务逻辑
异常类型?
返回友好错误提示
记录日志 + 返回通用错误

三层防护,各司其职:

  1. 全局异常处理器:兜底所有未处理的异常,记录日志,返回统一格式
  2. 业务异常处理:处理可预期的业务错误,返回友好提示
  3. 端点级 try-except:处理特定接口的特定异常,做细粒度恢复

定义业务异常类

首先,定义一套业务异常类:

# app/core/exceptions.py
from typing import Optional, Any

class AppException(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
        self.details = details
        super().__init__(message)


class ValidationError(AppException):
    """数据验证错误"""
    def __init__(self, message: str, details: Any = None):
        super().__init__(message, "VALIDATION_ERROR", 400, details)


class NotFoundError(AppException):
    """资源不存在"""
    def __init__(self, resource: str, resource_id: str):
        super().__init__(
            f"{resource} not found: {resource_id}",
            "NOT_FOUND",
            404,
            {"resource": resource, "id": resource_id}
        )


class GenerationError(AppException):
    """AI 生成错误"""
    def __init__(self, message: str, model: str = None):
        super().__init__(
            message,
            "GENERATION_ERROR",
            500,
            {"model": model}
        )


class ExternalServiceError(AppException):
    """外部服务错误"""
    def __init__(self, service: str, message: str):
        super().__init__(
            f"{service} error: {message}",
            "EXTERNAL_SERVICE_ERROR",
            502,
            {"service": service}
        )

有了这套异常类,错误就有了明确的分类和结构。


全局异常处理器

然后,在 FastAPI 应用中注册异常处理器:

# app/main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.core.exceptions import AppException
from app.core.logger import logger
import traceback

app = FastAPI()

@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
    """处理业务异常"""

    # 业务异常用 warning 级别
    logger.warning(
        f"业务异常 [{exc.code}]: {exc.message}",
        extra={
            "path": request.url.path,
            "method": request.method,
            "code": exc.code,
            "details": exc.details
        }
    )

    return JSONResponse(
        status_code=exc.status_code,
        content={
            "success": False,
            "error": {
                "code": exc.code,
                "message": exc.message,
                "details": exc.details
            }
        }
    )


@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """处理所有未捕获的异常"""

    # 未知异常用 error 级别,记录完整堆栈
    logger.error(
        f"未处理的异常: {type(exc).__name__}: {str(exc)}",
        extra={
            "path": request.url.path,
            "method": request.method,
            "traceback": traceback.format_exc()  # 完整堆栈!
        }
    )

    # 生产环境不暴露内部错误细节
    return JSONResponse(
        status_code=500,
        content={
            "success": False,
            "error": {
                "code": "INTERNAL_ERROR",
                "message": "An internal error occurred. Please try again later."
            }
        }
    )

关键点traceback.format_exc() 会记录完整的堆栈信息。

有了这个,再也不用加 print 去猜异常在哪了。


端点级异常处理

对于特定接口,可以做更细粒度的处理:

# app/api/endpoints/generate.py
from app.core.exceptions import GenerationError, ValidationError

@router.post("/shot-image")
async def generate_shot_image(request: GenerateShotImageRequest):
    """生成分镜图片"""

    # 参数验证
    if not request.prompt.strip():
        raise ValidationError("Prompt cannot be empty")

    try:
        adapter = ImageGenerationAdapterFactory.get_current_adapter()
        result = await adapter.generate_shot_image(
            prompt=request.prompt,
            width=request.width,
            height=request.height
        )

        if not result.get("success"):
            raise GenerationError(
                result.get("error", "Unknown generation error"),
                model=settings.IMAGE_MODEL
            )

        return {"success": True, "data": result}

    except GenerationError:
        raise  # 已经是业务异常,直接抛出

    except torch.cuda.OutOfMemoryError:
        # 特定异常转换为业务异常
        raise GenerationError("GPU out of memory. Please try a smaller image size.")

    except Exception as e:
        # 未知异常,记录后转换为业务异常
        logger.error(f"Unexpected error in generate_shot_image: {e}")
        raise GenerationError(f"Generation failed: {str(e)}")

这样:

  • 参数错误返回 400
  • 显存不足返回 500,但有友好提示
  • 其他异常也会被捕获,不会"消失"

日志配置

小禾选用了 loguru,比标准库的 logging 好用太多:

# app/core/logger.py
from loguru import logger
import sys

# 移除默认处理器
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"      # 压缩旧日志
)

# 错误日志单独文件
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"
)

错误日志单独存放,方便排查问题。


请求日志中间件

小禾还加了个中间件,记录每个请求的基本信息:

import time
import uuid

@app.middleware("http")
async def log_requests(request: Request, call_next):
    """记录请求日志"""

    # 生成请求 ID
    request_id = str(uuid.uuid4())[:8]

    # 记录请求开始
    logger.info(f"[{request_id}] --> {request.method} {request.url.path}")

    start = time.time()

    # 处理请求
    response = await call_next(request)

    # 计算耗时
    duration = time.time() - start

    # 记录请求完成
    logger.info(f"[{request_id}] <-- {response.status_code} in {duration:.2f}s")

    # 添加响应头,方便前端追踪
    response.headers["X-Request-ID"] = request_id

    return response

日志输出:

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

有了请求 ID,前端报错时只要把 ID 发过来,就能快速定位日志。


开发环境返回详细错误

生产环境不能暴露内部错误,但开发环境可以:

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """处理所有未捕获的异常"""

    error_detail = {
        "code": "INTERNAL_ERROR",
        "message": str(exc)
    }

    # 开发环境返回堆栈
    if settings.DEBUG:
        error_detail["type"] = type(exc).__name__
        error_detail["traceback"] = traceback.format_exc().split("\n")

    logger.error(
        f"未处理的异常: {type(exc).__name__}: {str(exc)}",
        extra={"traceback": traceback.format_exc()}
    )

    return JSONResponse(
        status_code=500,
        content={"success": False, "error": error_detail}
    )

开发环境的响应:

{
    "success": false,
    "error": {
        "code": "INTERNAL_ERROR",
        "message": "'NoneType' object has no attribute 'prompt'",
        "type": "AttributeError",
        "traceback": [
            "Traceback (most recent call last):",
            "  File \"app/api/endpoints/generate.py\", line 42, in generate_shot_image",
            "    prompt = request.data.prompt",
            "AttributeError: 'NoneType' object has no attribute 'prompt'"
        ]
    }
}

一眼就能看出问题在哪。


禁止吞异常

最后,小禾在团队立下一条规矩:

禁止写 except: pass

如果真的要忽略某个异常,至少打个日志:

# 错误写法
try:
    something()
except Exception:
    pass  # 禁止!

# 正确写法
try:
    something()
except SomeSpecificError as e:
    logger.debug(f"忽略的异常: {e}")  # 至少留个记录

更好的做法是只捕获特定异常:

# 最佳实践
try:
    result = maybe_fail()
except FileNotFoundError:
    result = default_value
except PermissionError as e:
    logger.warning(f"权限不足: {e}")
    raise
# 其他异常会自动向上抛出

异常处理清单

小禾总结了一份清单:

原则实践
不要吞异常except: pass 是禁止的
分层处理全局 -> 业务 -> 端点
日志要完整包含堆栈、请求信息
错误码标准化定义业务异常枚举
开发/生产区分开发环境暴露详情
请求可追踪添加 request_id

小禾的感悟

那三小时的排查,
让我学会一个道理:

异常不会消失,
只会被藏起来。

except pass 是懒惰,
是给未来的自己挖坑。

每个异常都有它的意义,
要么处理它,
要么记录它,
要么抛出它。

但绝不能忽视它。

全局处理器是保险,
业务异常是分类,
日志是证据。

有了这三样,
再也不用三小时找 bug 了。

下次有人说 500,
我只需要一分钟。

小禾看着清晰的日志输出,心情大好。

三小时的教训,换来了一套完善的异常处理体系。

值了。


下一篇预告:前端传了个 null,后端直接炸了

防御性编程,让你的接口固若金汤。

敬请期待。

在造纸企业智能化转型过程中,实现三层架构模型中的信息物理融合系统是关键。信息物理融合系统(Cyber-Physical Systems, CPS)是工业4.0的核心组成部分,它通过物理系统与数字技术的紧密集成来实现生产过程的优化和智能化。针对这一问题,可以按照以下步骤来实施: 参考资源链接:[造纸企业智能化转型:迈向工业4.0的蓝图与策略](https://wenku.youkuaiyun.com/doc/3rt5cpge8i?spm=1055.2569.3001.10343) 1. 明确三层架构:首先要确保管理决策层、生产管理层和生产操作层三个层次的信息流和控制流的无缝对接。管理决策层需集成ERP、SCM、CRM和PLM系统,生产管理层以智能工厂为中心,确保产品全生命周期管理,而生产操作层则要充分应用物联网技术实现设备的实时感知和控制。 2. 构建数据平台:建立统一的数据管理平台,实现数据的采集、存储、处理和分析。这包括引入边缘计算来就近处理生产现场的实时数据,以及使用云计算对大规模数据进行存储和深度分析。 3. 集成信息物理系统:在生产管理层中部署信息物理系统(CPS),这通常包括传感器、执行器、控制单元和软件算法等,这些组件协同工作,实现对生产过程的实时监测和智能控制。 4. 应用智能算法:利用人工智能、机器学习等智能算法对收集到的数据进行分析,以实现预测维护、质量控制、生产调度等智能决策支持。 5. 实施虚拟现实与仿真:在技术愿景中提到的虚拟现实(VR)和建模与仿真技术能够帮助企业在生产前进行模拟,优化生产流程和设备布局,减少实际生产中的风险和成本。 6. 持续改进与优化:智能化转型是一个持续的过程,需要根据实施效果不断进行调整和优化,包括对硬件设备的升级、软件算法的改进以及管理流程的优化。 通过上述步骤,造纸企业可以在生产过程中实现信息与物理世界的深度融合,从而提高生产效率,降低成本,并提高对市场变化的响应速度。推荐参考《造纸企业智能化转型:迈向工业4.0的蓝图与策略》以获取更深入的理解和实用的转型策略。 参考资源链接:[造纸企业智能化转型:迈向工业4.0的蓝图与策略](https://wenku.youkuaiyun.com/doc/3rt5cpge8i?spm=1055.2569.3001.10343)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员义拉冠

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值