Structured Logging — 本质、数据格式及在应用程序中的工作原理

作者: IT Sectr 发布日期: 2026-05-28 阅读时间: 8 分钟

Structured Logging 是一种日志记录方法,其中每条消息以机器可读格式呈现,采用键值对的形式,而非非结构化文本。与平面字符串不同,结构化日志包含元数据:timestamp、级别、模块、请求标识符 — 并且可以被分析系统索引。根据 O'Reilly Effective Logging 的数据,转向结构化格式可以将事故查找时间从几个小时缩短到几分钟,这得益于按字段进行筛选的能力。这是现代移动和服务器开发中的事实上的标准:JSON 和 logfmt 可以让程序处理日志,而非用眼睛查看。

主要观点

  • Structured Logging — 以键值对格式而非平面文本展示日志,适合自动化处理
  • JSON — 最常见的结构化日志格式,所有现代采集和分析系统均支持
  • Logfmt — Heroku 的紧凑格式,适合人类阅读和通过 grep 解析
  • ELK Stack — Elasticsearch、Logstash、Kibana — 存储和可视化结构化日志的标准基础设施
  • 上下文 — 请求标识符、用户会话、应用程序版本 — 每条结构化消息的必填字段

什么是 Structured Logging

Structured Logging 是一种日志记录方法,其中每条消息包含命名字段和类型化的值。不是像 User 42 logged in from device ABC 这样的字符串,而是一组字段:user_id=42, event=login, device_id=ABC, timestamp=2026-07-04T10:30:00Z

结构化日志相比文本日志的主要优势是可以进行程序化处理。解析文本日志需要正则表达式和对字符串格式的假设。结构化日志可以无损地解析:每个字段都有已知的类型和名称,可以构建询问如 查找用户42在过去一小时内的所有授权错误,无需额外处理。

根据 Honeycomb.io (2023) 的数据,在生产环境中使用 structured logging 的团队平均检测事故的速度是依赖文本日志和 grep 的团队的 4 倍。

结构化日志的格式

Structured Logging 支持多种序列化格式。选择取决于基础设施:JSON 适合与 Elasticsearch 和云系统集成,logfmt 适合通过 tail 和 grep 进行终端查看,Protocol Buffers 适合高性能、带宽受限的系统。

格式示例何时使用
JSON{"event":"login","user_id":42}ELK Stack、云采集器、微服务
Logfmtevent=login user_id=42 duration_ms=150终端、tail、heroku logs
MessagePackJSON 的二进制等价物高负荷系统、IoT

JSON — 通用格式

JSON 是结构化日志最常见的格式。所有采集系统都原生支持:Logstash、Fluentd、Amazon CloudWatch、Google Cloud Logging。JSON 日志易于人类阅读,也可以被任何编程语言解析,无需额外库。主要缺点是冗余:每个键值对都需要引号和冒号,与 logfmt 相比存储数据量增加 30–50%。

Logfmt — 紧凑格式

Logfmt 由 Heroku 开发,适合终端查看。比 JSON 更紧凑,保持人类可读性,可以通过 cutawk 轻松截取。示例:ts=2026-07-04T10:30:00Z level=error module=api status=500。Logfmt 不需要对大多数字符进行转义,适合容器中的 stdout 日志记录。

为什么移动开发中需要 Structured Logging

在移动应用程序中,Structured Logging 解决三个关键问题:不需要在设备上复现就能找到崩溃原因、跟踪用户会话以及按应用程序版本分析性能。

移动设备上的文本日志几乎没有用处 — 开发人员无法在用户设备上 grep 日志。结构化日志被发送到云系统(Firebase、Sentry、Datadog)并在那里被索引。可以构建查询:显示 iOS 17.4 上所有 crash,应用程序版本 3.2,在 checkouts 模块中,几秒内就能得到精确结果。

根据 Sentry (2024) 的数据,使用 structured breadcrumbs 的应用程序在每个崩溃报告中比只记录错误文本的应用程序多 60% 的上下文信息。这直接影响修复错误的速度。

采集和分析工具

ELK Stack — Elasticsearch、Logstash、Kibana — 仍然是处理结构化日志的标准基础设施。Logstash 接收 JSON 格式的日志,转换并发送到 Elasticsearch 进行索引,Kibana 提供查询和仪表板的可视化界面。

对于移动应用程序,云解决方案很受欢迎:Firebase Crashlytics 支持自定义日志,Sentry 支持 breadcrumbs,Datadog 支持 APM 跟踪。它们直接从移动 SDK 接收结构化日志,无需部署自己的后端。Firebase 提供免费的崩溃报告包,Sentry 提供分布式追踪,Datadog 与 APM 集成,可以同时监控客户端和服务器上的请求性能。

Grafana Loki — 针对日志优化的 Elasticsearch 替代方案。Loki 默认不索引消息内容,而是使用标签(labels)进行筛选。这样存储成本更低,对固定字段集的查询更快。

swift
// 通过 Swift Logger 将结构化日志输出为 JSON
struct StructuredLog {
    let event: String
    let attributes: [String: Any]
    let level: String

    func serialize() -> String {
        var base = "event=\(event) level=\(level)"
        for (key, value) in attributes {
            base += " \(key)=\(value)"
        }
        return base
    }
}

Structured Logging 最佳实践

结构化日志的第一条规则:每条消息必须包含请求或会话标识符。没有上下文,单条日志就没有用处 — 无法知道它属于哪个用户或请求。在会话开始时添加 correlation ID,并通过应用程序的所有层传递它。

第二条规则:字段类型化。数值字段(duration_ms、status_code、retry_count)必须作为数字传递,而不是字符串。Elasticsearch 和类似系统对数字和字符串的索引方式不同:数字可以进行聚合(平均值、中位数、百分位数),字符串则进行全文搜索。错误的类型化会导致无法构建分析仪表板。

第三条规则:避免嵌套对象。嵌套深度超过 2 层的 JSON 日志难以筛选和可视化。代替 {"user": {"name": "Alice", "role": "admin"}},使用平面键:user_name=Alice user_role=admin

kotlin
// 在 Android 上通过 Timber + logfmt 进行结构化日志
class StructuredTree : Timber.Tree() {
    override fun log(priority: Int, tag: String?,
                  message: String?, t: Throwable?) {
        val level = priorityToLevel(priority)
        val logfmt = "level=$level tag=$tag message=$message"
        sendToRemote(logfmt)
    }
}

哪些字段是必填的

每条结构化消息的最小字段集:timestamp(ISO 8601)、level(debug/info/warn/error/fatal)、logger(模块或类名称)、message(人类可读的事件描述)。附加:correlation_id、user_id(如果知道)、version(应用程序版本)、platform(iOS/Android)、environment(dev/staging/prod)。

没有 correlation_id,结构化日志将变成一组分散的记录,无法关联到一个用户场景。在每次应用程序启动时生成 UUID,并将其添加到所有会话日志中。在实践中,correlation_id 必须通过所有层传递:从 UI 事件到网络请求和后台任务 — 否则部分日志将缺少上下文,无法参与分析。在端到端追踪中,一个 UUID 就可以收集用户路径的完整图景。

Swift 和 Kotlin 中的结构化日志示例

iOS 上,可以通过对 os_log 的封装来实现结构化日志,将字段序列化为 logfmt 格式。在 Android 上,可以通过 Timber 与自定义 Tree 实现,在发送到服务器前将消息转换为 JSON 或 logfmt。

swift
import OSLog

struct StructuredLogger {
    let subsystem: String
    let category: String

    func log(level: OSLogType,
              event: String,
              context: [String: Any]) {
        let oslogger = Logger(
            subsystem: subsystem,
            category: category
        )
        let fields = context.map {
            "\($0.key)=\($0.value)"
        }.joined(separator: " ")
        oslogger.log(level: level,
                     "\(event) \(fields)")
    }
}

Structured vs Unstructured:方法比较

选择结构化日志还是文本日志取决于项目阶段。在开发早期,文本日志更简单、更快速 — 开发人员直接编写消息,无需额外封装。但一旦项目超过了一个团队或一台服务器的边界,结构化日志就成为必须。

指标文本日志结构化日志
可读性终端中很高中等(需要 pretty-print)
搜索按子串 grep按字段和值查询
聚合不支持平均值、中位数、百分位数
集成需要解析ELK/Loki/Datadog 原生支持
存储体积较小(无元数据)较大(字段 + 值)

常见问题

移动日志记录用什么格式最好?

发送到服务器时使用 JSON — Firebase Crashlytics、Sentry 和 Datadog 原生支持。在 Xcode 或 Android Studio 日志中本地查看时使用 logfmt — 更紧凑,无需格式化即可阅读。

需要在客户端上使用结构化格式记录日志吗?

是的,客户端的结构化日志可以为每个崩溃报告添加 上下文:操作系统版本、网络状态、用户最后操作。没有结构化 breadcrumbs,崩溃报告只包含调用栈,缺少用户场景。

Logfmt 和 JSON 有什么区别?

Logfmt 更紧凑(体积减少 30–50%),在终端中更容易阅读。JSON 支持嵌套对象和数组,但需要转义引号。选择取决于基础设施:ELK 用 JSON,终端查看用 logfmt。

如何将 correlation ID 添加到所有日志?

在应用程序启动时创建 一个实例 UUID,存储在单例或 DI 容器中,并通过构造函数传递给所有日志器。替代方案是在 Kotlin 协程中使用 threading-local 或 Continuation Local Storage。

可以混合使用结构化和文本日志吗?

可以,但不建议 — 混合会导致无法自动索引。如果部分日志是文本,就必须用正则表达式解析,这会降低搜索性能和可靠性。最好将所有日志迁移到结构化格式。

总结

  • Structured Logging — 以键值对形式记录日志,适合自动索引和查询,与文本日志不同
  • JSON 和 logfmt — 主要格式:JSON 通用于采集系统,logfmt 紧凑适合终端查看和 Docker 日志
  • Correlation ID — 每条结构化消息的必填字段,无此字段日志无法关联到用户会话
  • ELK Stack 和 Grafana Loki — 存储、索引和可视化结构化日志的标准基础设施解决方案
  • 性能 — 使用 structured logging 的团队检测事故的速度是使用 grep 搜索文本的 4 倍
  • 类型化 — 数字必须作为数字传递,而不是字符串,以便在分析系统中进行聚合(平均值、中位数、百分位数)

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读