Structured Logging 是一种日志记录方法,其中每条消息以机器可读格式呈现,采用键值对的形式,而非非结构化文本。与平面字符串不同,结构化日志包含元数据:timestamp、级别、模块、请求标识符 — 并且可以被分析系统索引。根据 O'Reilly Effective Logging 的数据,转向结构化格式可以将事故查找时间从几个小时缩短到几分钟,这得益于按字段进行筛选的能力。这是现代移动和服务器开发中的事实上的标准:JSON 和 logfmt 可以让程序处理日志,而非用眼睛查看。
主要观点
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、云采集器、微服务 |
| Logfmt | event=login user_id=42 duration_ms=150 | 终端、tail、heroku logs |
| MessagePack | JSON 的二进制等价物 | 高负荷系统、IoT |
JSON 是结构化日志最常见的格式。所有采集系统都原生支持:Logstash、Fluentd、Amazon CloudWatch、Google Cloud Logging。JSON 日志易于人类阅读,也可以被任何编程语言解析,无需额外库。主要缺点是冗余:每个键值对都需要引号和冒号,与 logfmt 相比存储数据量增加 30–50%。
Logfmt 由 Heroku 开发,适合终端查看。比 JSON 更紧凑,保持人类可读性,可以通过 cut 和 awk 轻松截取。示例:ts=2026-07-04T10:30:00Z level=error module=api status=500。Logfmt 不需要对大多数字符进行转义,适合容器中的 stdout 日志记录。
在移动应用程序中,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 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
}
}
结构化日志的第一条规则:每条消息必须包含请求或会话标识符。没有上下文,单条日志就没有用处 — 无法知道它属于哪个用户或请求。在会话开始时添加 correlation ID,并通过应用程序的所有层传递它。
第二条规则:字段类型化。数值字段(duration_ms、status_code、retry_count)必须作为数字传递,而不是字符串。Elasticsearch 和类似系统对数字和字符串的索引方式不同:数字可以进行聚合(平均值、中位数、百分位数),字符串则进行全文搜索。错误的类型化会导致无法构建分析仪表板。
第三条规则:避免嵌套对象。嵌套深度超过 2 层的 JSON 日志难以筛选和可视化。代替 {"user": {"name": "Alice", "role": "admin"}},使用平面键:user_name=Alice user_role=admin。
// 在 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 就可以收集用户路径的完整图景。
在 iOS 上,可以通过对 os_log 的封装来实现结构化日志,将字段序列化为 logfmt 格式。在 Android 上,可以通过 Timber 与自定义 Tree 实现,在发送到服务器前将消息转换为 JSON 或 logfmt。
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)")
}
}
选择结构化日志还是文本日志取决于项目阶段。在开发早期,文本日志更简单、更快速 — 开发人员直接编写消息,无需额外封装。但一旦项目超过了一个团队或一台服务器的边界,结构化日志就成为必须。
| 指标 | 文本日志 | 结构化日志 |
|---|---|---|
| 可读性 | 终端中很高 | 中等(需要 pretty-print) |
| 搜索 | 按子串 grep | 按字段和值查询 |
| 聚合 | 不支持 | 平均值、中位数、百分位数 |
| 集成 | 需要解析 | ELK/Loki/Datadog 原生支持 |
| 存储体积 | 较小(无元数据) | 较大(字段 + 值) |
常见问题
发送到服务器时使用 JSON — Firebase Crashlytics、Sentry 和 Datadog 原生支持。在 Xcode 或 Android Studio 日志中本地查看时使用 logfmt — 更紧凑,无需格式化即可阅读。
是的,客户端的结构化日志可以为每个崩溃报告添加 上下文:操作系统版本、网络状态、用户最后操作。没有结构化 breadcrumbs,崩溃报告只包含调用栈,缺少用户场景。
Logfmt 更紧凑(体积减少 30–50%),在终端中更容易阅读。JSON 支持嵌套对象和数组,但需要转义引号。选择取决于基础设施:ELK 用 JSON,终端查看用 logfmt。
在应用程序启动时创建 一个实例 UUID,存储在单例或 DI 容器中,并通过构造函数传递给所有日志器。替代方案是在 Kotlin 协程中使用 threading-local 或 Continuation Local Storage。
可以,但不建议 — 混合会导致无法自动索引。如果部分日志是文本,就必须用正则表达式解析,这会降低搜索性能和可靠性。最好将所有日志迁移到结构化格式。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。