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,观测云 DataKit 采集,监控器告警,APM trace_id 打通链路。
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 日志采集与分析
FastAPI 通常以 uvicorn/gunicorn 部署在容器中,接入观测云的标准路径:
- 应用侧:按上文配置 JSON 格式日志输出到 stdout(容器最佳实践,不写文件)。
- DataKit 采集:容器环境 DataKit 自动采集 stdout;主机部署则在
conf.d/log/logging.conf配置文件采集,source设为fastapi-app、service设为服务名。JSON 日志自动解析成字段。 - Pipeline 标准化:提取状态码映射标准
status字段(5xx →error、4xx →warning),time取日志内时间戳,保证事件时间准确。 - 检索分析:日志查看器按
service、status、路径过滤;图表模式统计接口耗时分布与错误率趋势;聚类分析快速发现新增错误模式。 - 告警:监控 > 监控器 > 新建日志检测监控器,对
status:error或 5xx 访问日志设阈值,告警策略绑定钉钉/企业微信/飞书通知对象。 - 链路关联:接入观测云 APM(OpenTelemetry 探针)后 trace_id 自动注入日志,错误日志一键跳转完整调用链,慢请求定位从小时级降到分钟级。
- 成本控制:访问日志量大,走低频索引 + 短保留;错误日志走标准索引 + 长保留;历史数据转发归档到对象存储。
常见问题(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 输出,平台侧观测云 DataKit 的采集配置和 Pipeline 规则可以完全复用,只是 service 名称不同。
系列阅读
- 上一篇:Python 日志库六款横向对比
- 相关阅读:Flask 日志实战 | Python 日志最佳实践十条 | 什么是日志管理