FastAPI 日志实战:配置、请求中间件与 JSON 结构化输出

FastAPI 日志实战中文指南:FastAPI 为什么需要手动配置日志、dictConfig 集中配置、自定义 JsonFormatter、uvicorn 访问日志管理、请求日志中间件、异常堆栈记录,以及如何用观测云 DataKit 采集 FastAPI 日志实现检索、告警与链路关联。

最佳实践
FastAPI 日志实战:配置、请求中间件与 JSON 结构化输出技术指南封面

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 只配置了自己的 uvicornuvicorn.accessuvicorn.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"}

两个关键点:

  1. disable_existing_loggers: False:必须加上,否则 dictConfig 会把 uvicorn 已创建的 logger 禁用,访问日志消失。
  2. 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 部署在容器中,接入观测云的标准路径:

  1. 应用侧:按上文配置 JSON 格式日志输出到 stdout(容器最佳实践,不写文件)。
  2. DataKit 采集:容器环境 DataKit 自动采集 stdout;主机部署则在 conf.d/log/logging.conf 配置文件采集,source 设为 fastapi-appservice 设为服务名。JSON 日志自动解析成字段。
  3. Pipeline 标准化:提取状态码映射标准 status 字段(5xx → error、4xx → warning),time 取日志内时间戳,保证事件时间准确。
  4. 检索分析:日志查看器按 servicestatus、路径过滤;图表模式统计接口耗时分布与错误率趋势;聚类分析快速发现新增错误模式。
  5. 告警:监控 > 监控器 > 新建日志检测监控器,对 status:error 或 5xx 访问日志设阈值,告警策略绑定钉钉/企业微信/飞书通知对象。
  6. 链路关联:接入观测云 APM(OpenTelemetry 探针)后 trace_id 自动注入日志,错误日志一键跳转完整调用链,慢请求定位从小时级降到分钟级。
  7. 成本控制:访问日志量大,走低频索引 + 短保留;错误日志走标准索引 + 长保留;历史数据转发归档到对象存储。

常见问题(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.accessuvicorn.error

异步路由里写日志会阻塞事件循环吗?

标准库 Handler 是同步 I/O,超高并发下可能有影响。优化:用 QueueHandler + QueueListener 把日志写入挪到后台线程,或确保只写 stdout(管道写入很快)。绝大多数业务系统感知不到差异,不必过早优化。

FastAPI 和 Flask 的日志方案能统一吗?

能。两者都基于标准库 logging:统一用 dictConfig、统一 JSON 格式、统一 stdout 输出,平台侧观测云 DataKit 的采集配置和 Pipeline 规则可以完全复用,只是 service 名称不同。

系列阅读


获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台