Skip to content

日志与错误处理

print 在开发时够用,但上线后日志要写文件、要带时间戳、要能按级别过滤、要能被日志平台(ELK、Loki)收集。Python 标准库 logging 做这些事。配合 FastAPI 的全局异常处理,让线上问题能追到、错误响应统一、堆栈不泄露给前端。

一、logging 配置

logging.basicConfig 适合小脚本。FastAPI 项目里用字典配置更灵活——文件按天轮转、控制台和文件同时输出、不同级别分开:

python
import logging
from logging.handlers import TimedRotatingFileHandler

def setup_logging():
    """配置全局日志。在应用启动时调用一次。"""
    formatter = logging.Formatter(
        fmt="%(asctime)s %(levelname)s [%(name)s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    # 文件处理器:每天轮转,保留 30 天
    file_handler = TimedRotatingFileHandler(
        filename="logs/app.log",
        when="midnight",
        backupCount=30,
        encoding="utf-8",
    )
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.INFO)

    # 控制台处理器:开发时看输出
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    console_handler.setLevel(logging.DEBUG)

    # 配置根 logger
    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)
    root_logger.addHandler(file_handler)
    root_logger.addHandler(console_handler)

TimedRotatingFileHandler 按时间轮转日志文件——when="midnight" 每天午夜切一个新文件,backupCount=30 保留 30 天的旧日志。不轮转的日志文件会一直增长,把磁盘撑满

用的时候在每个模块里拿 logger:

python
import logging

logger = logging.getLogger(__name__)  # __name__ 是模块名,日志里能看到来源


@app.post("/articles")
def create_article(article: ArticleCreate, db: Session = Depends(get_db)):
    logger.info("创建文章 title=%s user_id=%s", article.title, current_user_id)

    new_article = Article(title=article.title, content=article.content)
    db.add(new_article)
    db.commit()

    logger.info("文章创建成功 id=%s", new_article.id)
    return {"id": new_article.id}

logger.info("msg %s", value) 用占位符而不是 f-string——日志级别被过滤时(比如设成 WARNING,INFO 不输出),占位符的值不会被格式化,省一点开销。更重要的是,日志平台能按字段解析。

日志级别

级别什么时候用
DEBUG调试细节(变量值、SQL 语句)
INFO正常流程(请求开始、操作完成)
WARNING不正常但不影响运行(磁盘快满、重试了一次)
ERROR出错了(接口失败、异常捕获)
CRITICAL系统级故障(数据库连不上、服务不可用)

生产环境通常设 INFO——DEBUG 太多,ERROR 太少。

二、结构化日志

文本日志人能读,但日志平台(ELK、Grafana Loki)按字段搜索和聚合时,解析非结构化文本很费劲。结构化日志输出 JSON,每个字段直接可查:

python
import json
import logging


class JsonFormatter(logging.Formatter):
    """把日志格式化成 JSON。"""

    def format(self, record):
        log_data = {
            "time": self.formatTime(record),
            "level": record.levelname,
            "module": record.module,
            "message": record.getMessage(),
        }

        # 异常信息
        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)

        return json.dumps(log_data, ensure_ascii=False)


# 换成 JSON 格式
file_handler.setFormatter(JsonFormatter())

输出长这样:

json
{"time": "2024-06-01 10:30:15", "level": "INFO", "module": "main", "message": "创建文章 title=Python 入门"}

日志平台收到这种格式,直接按 levelmodulemessage 字段查询和过滤,不用写正则解析。

三、全局异常处理

接口里可能抛各种异常——数据库连接失败、参数缺失、业务逻辑错误。15 篇提了 BusinessError,这里做更完整的方案。

业务异常和系统异常分开处理

python
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError


# 业务异常:预期的错误,返回给前端有意义的信息
class BusinessError(Exception):
    def __init__(self, code: int, message: str):
        self.code = code
        self.message = message


# 业务异常处理器
@app.exception_handler(BusinessError)
async def handle_business_error(request: Request, exc: BusinessError):
    logger.warning("业务异常 path=%s code=%s message=%s", request.url.path, exc.code, exc.message)
    return JSONResponse(
        status_code=200,
        content={"code": exc.code, "message": exc.message},
    )


# 请求校验异常处理器(Pydantic 校验失败时触发)
@app.exception_handler(RequestValidationError)
async def handle_validation_error(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content={"code": 422, "message": "参数校验失败", "detail": exc.errors()},
    )


# 兜底异常处理器:所有未捕获的异常
@app.exception_handler(Exception)
async def handle_unexpected_error(request: Request, exc: Exception):
    # 记录完整堆栈到日志,方便排查
    logger.error("未预期异常 path=%s", request.url.path, exc_info=True)

    # 返回给前端的是通用错误信息,不暴露堆栈
    return JSONResponse(
        status_code=500,
        content={"code": 500, "message": "服务器内部错误"},
    )

三层处理:业务异常(用户操作不当)→ 校验异常(参数格式不对)→ 兜底异常(代码 bug 或系统故障)。兜底处理器里 exc_info=True 把完整堆栈记到日志,但返回给前端的只有"服务器内部错误"——Python 堆栈暴露给前端既有安全隐患,也帮不了用户。

在业务逻辑里用

python
@app.post("/articles")
def create_article(article: ArticleCreate, db: Session = Depends(get_db)):
    # 业务校验:标题不能跟已有文章重复
    existing = db.query(Article).filter(Article.title == article.title).first()
    if existing:
        raise BusinessError(code=1001, message="标题已存在")

    # 正常逻辑
    new_article = Article(title=article.title, content=article.content)
    db.add(new_article)
    db.commit()

    return {"code": 0, "data": {"id": new_article.id}}

业务校验不通过抛 BusinessError,全局处理器统一转成 {"code": 1001, "message": "标题已存在"}接口函数里不用写 try/except,异常处理全交给全局处理器——代码干净,格式统一。