为什么要用 logging 而不是 print
项目里到处用 print() 记录过程信息,会带来三个问题:
- 说不清什么时间、什么位置输出的——终端里只有一行文字,排错时很难定位
- 想关掉这些日志怎么办? 得把每一行
print找出来删掉或注释掉 - 没法按重要性过滤——调试信息、正常信息、错误信息混在一起
用 logging 模块
from fastapi import FastAPI
from fastapi.responses import FileResponse
import logging
# 配置日志的基本信息
logging.basicConfig(
level=logging.INFO, # 日志级别
format="%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
)
app = FastAPI()
@app.get("/")
def root():
logging.info("访问首页~")
return FileResponse("static/index.html")format 里的占位符:
| 占位符 | 含义 |
|---|---|
%(asctime)s | 时间 |
%(levelname)s | 日志级别 |
%(filename)s | 文件名 |
%(lineno)d | 行号 |
%(message)s | 日志信息 |
输出的样子:
2026-09-20 15:04:12,301 - INFO - [main.py:29] - 访问首页~日志级别
日志级别是给日志信息贴上的"重要性标签",常见级别从低到高:
DEBUG < INFO < WARNING < ERROR < FATALbasicConfig(level=...) 设成哪个级别,就只输出该级别及以上的日志。比如设 level=logging.ERROR,那些 logging.info(...) 就都不会打印了——一行配置就关掉了所有低级别日志,这正是 logging 比 print 强的地方。
| 级别 | 用途 |
|---|---|
| DEBUG | 调试细节(开发时用) |
| INFO | 正常运行的记录 |
| WARNING | 警告(还能跑,但要注意) |
| ERROR | 出错了 |
| FATAL | 致命错误 |
统一异常处理
项目功能多,不可能在每个接口里都写 try/except。FastAPI 提供了统一异常处理器:
from starlette.requests import Request
from starlette.responses import JSONResponse
@app.exception_handler(Exception)
def handle_exception(request: Request, exc: Exception):
logging.error(f"处理异常, 请求路径: {request.url}, 异常信息: {exc}")
return JSONResponse(content={"code": 500, "message": "服务器内部错误, 请联系管理员~", "data": None})| 要点 | 说明 |
|---|---|
@app.exception_handler(Exception) | 注册"捕获所有异常"的处理器(Exception 是所有异常的父类) |
request: Request | 出事的那次请求(可以从里面拿到 request.url) |
exc: Exception | 捕获到的异常对象 |
JSONResponse(...) | 返回的对象类型必须是 Response,用 JSONResponse 返回 JSON |
处理前后的对比:
{"code": 200, "message": "AI回复成功", "data": "..."}
{"code": 500, "message": "服务器内部异常", "data": null}NOTE
处理器里返回的 JSONResponse 没有设置 HTTP 状态码,所以浏览器/客户端拿到的 HTTP 状态码还是 200,只有响应体里的 code 是 500。这是本项目的约定(统一响应体里的业务码);如果想让 HTTP 状态码也是 500,写成 JSONResponse(status_code=500, content={...}) 即可。
TIP
统一异常处理的价值:接口报错时不再返回一大段 Python 堆栈,而是返回和正常响应同样格式的 JSON。前端只要判断 code 就能优雅提示用户,日志里也留下了具体是哪个路径、什么异常。
相关
练习题
一、知识回顾(读完直接做下面的实践题)
- 用 print 记录日志的问题:说不清时间和位置、想关掉要一行行删、无法按重要性过滤
- 配置日志:
logging.basicConfig(level=..., format=...) - format 里
%(asctime)s是时间、%(levelname)s是日志级别、%(filename)s是文件名、%(lineno)d是行号 - 日志级别从低到高:DEBUG、INFO、WARNING、ERROR、FATAL;设置
level后只输出该级别及以上的日志 - 注册统一异常处理器:
@app.exception_handler(Exception),函数形参一般是request: Request和exc: Exception - 异常处理器返回的对象类型必须是
Response,所以用JSONResponse返回 JSON
二、裸写题
- 提示(先自己想,实在想不出再点开)
一级 · 思路:先 basicConfig 一次(放在程序开头),之后到处用
logging.info/error二级 · 方法:logging.basicConfig(level=logging.INFO, format=...)三级 · 骨架:format 用"%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"- 参考答案(做完再点开)
python# 2-1 配置日志 import logging from fastapi import FastAPI from fastapi.testclient import TestClient logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s" ) app = FastAPI(title="优化练习") @app.get("/") def root(): logging.info("访问首页") return {"message": "ok"} if __name__ == "__main__": client = TestClient(app) print(client.get("/").json())- 提示(先自己想,实在想不出再点开)
一级 · 思路:级别设高了,低级别日志就被过滤掉 二级 · 方法:
level=logging.ERRORvslevel=logging.INFO三级 · 骨架:INFO < ERROR,所以设 ERROR 后 INFO 不输出- 参考答案(做完再点开)
python# 2-2:把 level 改成 logging.ERROR 再跑, # 就会看到 "访问首页" 这类 INFO 日志不再输出(INFO 低于 ERROR)- 提示(先自己想,实在想不出再点开)
一级 · 思路:对比"有/没有异常处理器"两次的响应 二级 · 方法:
@app.exception_handler(Exception)+JSONResponse三级 · 骨架:处理器里用logging.error记下request.url和异常信息- 参考答案(做完再点开)
python# 2-3 统一异常处理器(日志配置与 / 接口见 2-1) import logging from fastapi import FastAPI from fastapi.testclient import TestClient from starlette.requests import Request from starlette.responses import JSONResponse app = FastAPI(title="优化练习") @app.get("/boom") def boom(): logging.info("准备触发异常") return {"value": 1 / 0} # 故意抛异常 @app.exception_handler(Exception) def handle_exception(request: Request, exc: Exception): logging.error(f"处理异常, 请求路径: {request.url}, 异常信息: {exc}") return JSONResponse(content={"code": 500, "message": "服务器内部错误", "data": None}) if __name__ == "__main__": # TestClient 默认会把服务端异常直接抛出来(方便调试),关掉才能看到处理器的返回 client = TestClient(app, raise_server_exceptions=False) print(client.get("/boom").json()) # 被异常处理器兜住,返回 {code:500,...}
