FastAPI 日志实战:配置、请求中间件与 JSON 结构化输出
FastAPI 日志实战中文指南:FastAPI 为什么需要手动配置日志、dictConfig 集中配置、自定义 JsonFormatter、uvicorn 访问日志管理、请求日志中间件、异常堆栈记录,以及如何用观测云 DataKit 采集 FastAPI 日志实现检索、告警与链路关联。
FastAPI 是现代 Python Web 框架中增长最快的一个,但它没有自带日志系统——不像 Flask 有 app.logger、Django 有内置 LOGGING。FastAPI 应用中的 logger.info() 默认没有任何输出,必须由你用标准库 logging 显式配置。本文从原理到实战完整讲解 FastAPI 日志方案,并说明采集后的字段与请求关联检查。
核心要点速览
- FastAPI 不配置日志就没有日志:它把日志完全交给 Python 标准库 logging,什么都不配时 root logger 没有 handler,日志全部丢弃。
- dictConfig 是推荐的配置方式:在应用启动前一次性定义 formatter、handler、logger,同时接管 uvicorn 的日志。
- 请求日志用中间件统一记录:纯 ASGI 中间件记录 method/path/status/耗时,比在每个路由里写日志干净得多。
- 集中查询:JSON 输出到 stdout 后,检查采集端的字段解析;需要关联请求时,核对日志与 Trace 的标识。
FastAPI 的日志机制有什么特别之处?
FastAPI 本身不产生应用日志。你在路由里写:
from fastapi import FastAPI
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
@app.get("/")
def index():
logger.info("首页被访问") # 默认不会有任何输出!
return {"msg": "hello"}
运行后控制台只有 uvicorn 自己的启动日志,看不到"首页被访问"。原因:标准库 logging 的 root logger 默认级别 WARNING 且没有 handler;uvicorn 只配置了自己的 uvicorn、uvicorn.access、uvicorn.error 三个 logger,不会管你的业务 logger。
所以 FastAPI 日志的第一课:日志配置完全是你的责任。
如何用 dictConfig 配置 FastAPI 日志?
在创建应用之前集中配置:
from logging.config import dictConfig
from fastapi import FastAPI
dictConfig({
"version": 1,
"disable_existing_loggers": False, # 关键:保留 uvicorn 的 logger
"formatters": {
"default": {
"format": "%(asctime)s %(levelname)s [%(name)s] %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
},
},
"root": {"level": "INFO", "handlers": ["console"]},
})
logger = logging.getLogger("myapp")
app = FastAPI()
@app.get("/")
def index():
logger.info("首页被访问")
return {"msg": "hello"}
两个关键点:
disable_existing_loggers: False:必须加上,否则 dictConfig 会把 uvicorn 已创建的 logger 禁用,访问日志消失。- root 配了 handler 后,业务 logger 直接冒泡:
getLogger("myapp")不需要单独挂 handler。
如何接管 uvicorn 的访问日志?
uvicorn 的访问日志走 uvicorn.access logger。可以在 dictConfig 中统一它的格式和级别:
"loggers": {
"uvicorn.access": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
"uvicorn.error": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
},
启动时用 uvicorn main:app --log-config 指向配置文件也可以,但 dictConfig 在代码里更容易版本化。如果要关掉 uvicorn 默认访问日志改用自己的中间件日志,把 uvicorn.access 级别设为 WARNING 即可。
如何用中间件记录请求日志?
与其在每个路由函数里重复写日志,不如用一个纯 ASGI 中间件统一记录所有请求:
import time
from starlette.types import ASGIApp, Receive, Scope, Send
class LoggingMiddleware:
def __init__(self, app: ASGIApp):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
start = time.time()
status_code = [500]
async def send_wrapper(message):
if message["type"] == "http.response.start":
status_code[0] = message["status"]
await send(message)
try:
await self.app(scope, receive, send_wrapper)
finally:
duration = round((time.time() - start) * 1000, 2)
logger.info(
"request: %s %s status=%s duration_ms=%s client=%s",
scope["method"], scope["path"], status_code[0],
duration, scope["client"][0] if scope["client"] else "-",
)
app.add_middleware(LoggingMiddleware)
每个请求自动产生一条带方法、路径、状态码、耗时、客户端 IP 的结构化日志。需要 request_id 时,在中间件里生成 UUID 放进 scope["state"] 或响应头,并通过 contextvars 让所有业务日志自动携带。
如何记录异常堆栈?
@app.get("/pay")
def pay():
try:
process_payment()
except Exception:
logger.exception("支付处理失败")
raise
logger.exception() 自动附带完整堆栈。对于全局兜底,用 FastAPI 的异常处理器:
from fastapi.responses import JSONResponse
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
logger.exception("未处理异常: %s %s", request.method, request.url.path)
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
如何输出 JSON 结构化日志?
生产环境推荐 JSON 格式。两种做法:
方式一:python-json-logger
"formatters": {
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
}
}
方式二:自定义 JsonFormatter 类(零依赖):
import json, logging
class JsonFormatter(logging.Formatter):
def format(self, record):
payload = {
"timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
payload["exception"] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=False)
然后在 dictConfig 的 formatter 里 "()": "main.JsonFormatter" 引用它。输出:
{"timestamp": "2026-08-25T14:40:11+0800", "level": "INFO", "logger": "myapp", "message": "request: GET /api/users status=200 duration_ms=8.5"}
采集后检查字段与请求关联
将 FastAPI 日志接入观测云时,先明确输出位置:容器 stdout 使用容器日志采集,文件输出则按日志采集文档配置路径、source 和 Pipeline。用前面中间件产生的一条请求日志测试解析,检查时间、HTTP 状态码与耗时字段的类型。
接着发起一个已知失败的请求,确认错误日志能按服务和路径找到,再按日志检测文档设置检测条件。HTTP 状态码与日志级别应分别保留,便于区分业务错误和服务异常。
需要从错误日志进入调用链时,应用日志应记录探针提供的 trace_id,并核对与链路数据中的值一致;关联索引还需匹配 service、env、version。具体字段见 APM FAQ。仅安装探针不代表自定义 Formatter 已输出这些字段。
常见问题(FAQ)
FastAPI 里 logger.info() 没输出是什么原因?
标准库 root logger 没有配置 handler。FastAPI 不像 Flask 会帮你建 logger,必须在启动时调用 dictConfig(或 basicConfig)配置 handler 和级别。这是 FastAPI 日志最高频的"坑"。
dictConfig 之后 uvicorn 的日志不见了怎么办?
disable_existing_loggers 设成了 True(默认值),把 uvicorn 启动前创建的 logger 禁用了。改成 False 即可;如需自定义 uvicorn 日志格式,在 dictConfig 的 loggers 里显式配置 uvicorn.access 和 uvicorn.error。
异步路由里写日志会阻塞事件循环吗?
标准库 Handler 是同步 I/O,超高并发下可能有影响。优化:用 QueueHandler + QueueListener 把日志写入挪到后台线程,或确保只写 stdout(管道写入很快)。绝大多数业务系统感知不到差异,不必过早优化。
FastAPI 和 Flask 的日志方案能统一吗?
能。两者都基于标准库 logging:统一用 dictConfig、统一 JSON 格式、统一 stdout 输出,采集配置可作为共同起点;访问日志字段、时间格式和异常堆栈仍应各用一条样本验证。
系列阅读
- 上一篇:Python 日志库六款横向对比
- 相关阅读:Flask 日志实战 | Python 日志最佳实践十条 | 什么是日志管理