structlog 完全指南:Python 结构化日志框架详解

前言

日志(Logging)是每个应用程序必不可少的组成部分。

对于一个简单的 Python 程序,我们通常会这样写:

import logging

logging.basicConfig(level=logging.INFO)

logging.info("User login success")

或者:

logging.info("User %s login success", username)

这种方式对于开发阶段没有问题。

但是到了生产环境,你很快就会遇到各种问题:

  • 如何统计某个用户一天登录多少次?
  • 如何根据 request_id 找到一次完整请求?
  • 如何把日志发送到 Elasticsearch(一个开源的、基于 Apache Lucene 构建的分布式搜索与分析引擎)?
  • 如何让 Loki、Grafana、Datadog、Splunk 自动解析日志?
  • 如何让 AI 或脚本自动分析日志?

如果日志只是字符串,那么这些都会变得十分困难。

因此,现代云原生系统几乎都采用 Structured Logging(结构化日志)

而 Python 世界最流行的结构化日志库,就是 structlog。它自 2013 年以来已广泛用于生产环境,支持 JSON、logfmt、漂亮的控制台输出,并且可以与 Python 标准 logging 模块无缝集成。(structlog)


什么是 structlog?

structlog 是一个用于 结构化日志(Structured Logging) 的 Python 库。

官方网站给它的介绍非常简单:

Simple. Powerful. Fast. Pick three.

意思就是:

  • 简单(Simple)
  • 强大(Powerful)
  • 快速(Fast)

它最大的思想只有一句话:

日志不是字符串,而是一组 Key-Value 数据。 (structlog)

例如,不再记录:

User Arnold login success from 192.168.1.1

而是记录:

{
  "event": "user_login",
  "username": "Arnold",
  "ip": "192.168.1.1",
  "status": "success"
}

这就是结构化日志。


为什么需要结构化日志?

传统日志:

2025-07-10 10:12:31 INFO User Arnold login success

机器只能把它当成一整行字符串。

如果想统计:

Arnold 登录了多少次?

只能:

  • 正则表达式
  • 字符串切割
  • grep
  • awk

非常麻烦。

而结构化日志:

{
  "timestamp":"2025-07-10T10:12:31",
  "level":"info",
  "user":"Arnold",
  "action":"login"
}

任何日志平台都可以直接:

SELECT *
WHERE user="Arnold"

甚至:

GROUP BY user

完全不需要解析字符串。


structlog 的核心思想

structlog 认为:

日志事件就是一个 Python 字典。

例如:

{
    "event": "User login",
    "user": "Arnold",
    "ip": "127.0.0.1",
    "status": "success"
}

最后再决定:

输出成:

控制台:

User login user=Arnold ip=127.0.0.1

或者:

JSON:

{
  ...
}

或者:

logfmt:

event="User login" user=Arnold ip=127.0.0.1

所以:

数据和展示完全分离。


安装

安装非常简单:

pip install structlog

第一个 structlog 程序

import structlog

logger = structlog.get_logger()

logger.info(
    "user_login",
    username="Arnold",
    ip="192.168.1.100"
)

输出:

2025-07-10T10:20:11
event='user_login'
username='Arnold'
ip='192.168.1.100'

可以看到:

日志已经不是一句话。

而是一组字段。


event 是什么?

很多人第一次都会疑惑:

为什么第一个参数叫:

logger.info("user_login")

而不是 message?

实际上:

logger.info(
    "user_login",
    username="Arnold"
)

等价于:

{
    "event": "user_login",
    "username": "Arnold"
}

所以:

第一个参数就是:

event

表示:

发生了什么事情。

而不是完整句子。

推荐:

event="user_login"

event="payment_success"

event="create_order"

event="delete_user"

而不是:

"User Arnold login success."

bind():绑定上下文

这是 structlog 最强大的功能之一。

例如:

logger = structlog.get_logger()

logger = logger.bind(user="Arnold")

以后:

logger.info("login")

自动输出:

user=Arnold
event=login

再记录:

logger.info("logout")

输出:

user=Arnold
event=logout

无需每次重复写:

user="Arnold"

多次 bind()

可以不断增加上下文:

logger = logger.bind(
    request_id="12345"
)

logger = logger.bind(
    trace_id="abc"
)

以后:

logger.info("query")

自动包含:

user
request_id
trace_id

这对于微服务非常重要。


JSON 输出

生产环境最常见的是:

import structlog

structlog.configure(
    processors=[
        structlog.processors.JSONRenderer()
    ]
)

logger = structlog.get_logger()

logger.info(
    "login",
    user="Arnold"
)

输出:

{
  "event":"login",
  "user":"Arnold"
}

JSON 是目前日志平台最常见的格式之一,便于 Elasticsearch、Loki 等系统直接解析。(structlog)


Processor(处理器)

Processor 是 structlog 的核心。

它采用流水线(Pipeline)的方式。

日志

↓

增加时间

↓

增加等级

↓

增加异常

↓

JSON 输出

例如:

processors=[
    add_log_level,
    TimeStamper(),
    JSONRenderer()
]

每一个 Processor:

输入:

dict

输出:

dict

最后:

Renderer:

输出字符串。

所以整个流程十分灵活。


Context(上下文)

大型系统里:

一次请求通常需要:

request_id

trace_id

tenant_id

user_id

如果每次:

logger.info(
    request_id=...
)

会非常麻烦。

structlog 支持上下文绑定,让这些字段自动出现在日志中,并支持现代 Python 的 contextvars,能够在异步(asyncio)环境中安全地传播请求上下文。(structlog)

例如:

logger = logger.bind(
    request_id="abc"
)

以后:

所有日志:

自动带:

request_id

与 logging 的关系

很多新人误以为:

structlog 是:

logging 的替代品。

其实不是。

structlog 可以:

structlog

↓

logging

↓

Console

File

Syslog

CloudWatch(AWS(亚马逊云科技)提供的一项监控和可观测性服务)

...

也就是说:

它更像:

logging 的增强层。

官方也支持与标准库 logging 无缝集成,因此可以继续使用现有的 Handler、Formatter 等生态。(structlog)


在 FastAPI 中使用

例如:

logger.info(
    "http_request",
    path="/users",
    method="GET",
    status=200
)

输出:

{
  "event":"http_request",
  "path":"/users",
  "method":"GET",
  "status":200
}

Grafana、Loki 可以直接:

status=500

查询:

所有错误请求。


与 OpenTelemetry 配合

现代微服务:

通常:

Application

↓

structlog

↓

OpenTelemetry

↓

OTLP

↓

Grafana Loki

↓

Grafana Dashboard

日志中记录:

trace_id

span_id

request_id

即可与 Trace 关联。

点击一次 Trace:

可以直接看到:

对应日志。


structlog 的优点

1. 真正的结构化日志

不是:

字符串

而是:

dict

机器更容易理解和处理。


2. 上下文管理优秀

通过 bind()contextvars,无需重复传递公共字段,适合请求链路追踪和多租户场景。(structlog)


3. 输出格式灵活

支持:

  • Console
  • JSON
  • logfmt
  • 自定义 Renderer

4. 与 logging 完全兼容

不用推翻已有项目。

可以渐进迁移。


5. 非常适合云原生

几乎所有:

  • Kubernetes
  • Docker
  • Grafana
  • Loki
  • ELK
  • Datadog(一个商业化的云原生可观测性与监控平台(SaaS 服务),它把监控、日志、链路追踪、告警、安全等功能集成在一个统一的平台里。可以把它理解为一个"全家桶式"的监控解决方案

都推荐:

JSON 日志。


structlog 的不足

当然,structlog 也并非适用于所有场景。

学习成本高于 logging

Python 自带的 logging 使用广泛,很多开发者已经熟悉。

而 structlog 需要理解:

  • Processor
  • Context
  • Renderer
  • Event Dict

这些概念。


配置较多

一个真正的生产配置:

通常几十行。

不像:

logging.basicConfig(...)

那么简单。


小项目收益有限

如果只是:

几十行脚本:

print()

或者:

logging.info()

已经足够。

没有必要引入 structlog。


什么时候应该使用 structlog?

推荐使用:

  • 微服务
  • FastAPI
  • Django
  • Flask
  • AI Agent
  • RAG 系统
  • LangGraph
  • Kubernetes
  • Docker
  • OpenTelemetry
  • ELK / Loki

不推荐:

  • 一次性脚本
  • 学习 Demo
  • 小工具

总结

structlog 并不是一个简单的日志输出库,它更像是一套 结构化日志解决方案。它把日志从传统的字符串提升为可查询、可分析、可关联的数据对象,再通过 Processor 管道完成增强和渲染,最终输出为控制台文本、JSON 或其他格式。

对于现代 Python 服务,尤其是部署在 Kubernetes、使用 OpenTelemetry、Grafana、Loki、ELK 等可观测性平台的应用,采用结构化日志已经成为一种主流实践。而 structlog 凭借与标准 logging 的兼容性、灵活的 Processor 管道以及优秀的上下文管理能力,成为 Python 生态中最受欢迎的结构化日志框架之一。(structlog)

如果你的项目已经进入生产环境,或者需要更好的日志检索、分析和链路追踪能力,那么学习并使用 structlog 将是一项非常值得的投入。

更多推荐