日志支柱 · 最老也最被低估的那根

日志是最古老、最好上手、也最容易失控的支柱。这一章讲结构化日志为什么是分水岭、Loki「只索引标签」的思路为什么和 Prometheus 同源、日志分级的实用判据,以及日志与其他两根支柱的关系。

作者 David更新于 第 46 篇(共 52 篇)

日志支柱 · 最老也最被低估的那根

三大支柱的最后一根:日志。

它最古老(printf 就是日志)、最好上手(谁都会写)、但也最容易失控——很多团队的日志账单比业务服务器还贵,而排障时还是找不到想要的。

分水岭:结构化

**日志工程只有一条分水岭:它是给人读的字符串,还是给机器查的数据?

❌ 非结构化(传统)
2026-07-15 10:23:45 ERROR Failed to process order 12345 for user 678: timeout after 30s

✅ 结构化(JSON)
{"ts":"2026-07-15T10:23:45Z", "level":"error", "msg":"failed to process order",
 "order_id":"12345", "user_id":"678", "error":"timeout", "duration_ms":30000,
 "trace_id":"abc123", "service":"order-api"}

区别不是“好看”——是能不能查:

你想问非结构化结构化
“所有超时的错误”正则捞文本,脆弱(改一个字就废)error="timeout",精确
“user 678 的所有操作”grep 678 → 会误中 order_id、金额、任何含 678 的东西user_id="678",准确
“P99 处理时长”做不到(得从文本里抠数字)duration_ms 直接算
“跳到这条日志的 trace”做不到trace_id 一键跳

非结构化日志的根本问题:它把“数据”编码成了“散文”,而散文没法可靠地反解析。 你写日志时脑子里的那个结构(订单号、用户、错误类型),在拼成字符串的那一刻就丢失了——之后所有人都要靠正则去猜它。

结构化是这一章唯一的“必须做”。 其他都是优化,这个是及格线。

Loki 的思路:只索引标签

传统日志系统(如 ES)的做法:给日志内容建全文索引。强大,但极贵**——索引往往比原始数据还大。**

Loki 反过来:只索引少量标签,内容完全不索引。

{service="order-api", level="error"}   ← 只索引这些(和 Prometheus 一模一样的标签!)
|= "timeout"                            ← 内容靠"暴力扫描"过滤

这个取舍很硬核:

  • 写入极便宜 —— 不用建全文索引,日志压缩后直接存对象存储。成本可能是 ES 的十分之一。
  • 按标签查极快 —— 和 Prometheus 的倒排索引同源。
  • 全文搜索靠扫 —— 但因为标签已经把范围缩得很小了(比如只扫 order-api 服务、error 级别、最近 1 小时),扫的数据量其实不大。

关键洞见:Loki 赌的是“你几乎总是先用标签缩小范围,再在小范围里搜内容”。 这个假设在排障场景下基本成立——你从来不会“在全公司所有日志里搜一个词”,你总是从指标或 trace 跳过来的,已经知道是哪个服务、哪个时间段。

它的标签体系和 Prometheus 刻意一致,所以你可以用几乎一样的标签在两个系统间跳转。“让日志用指标的标签模型”是 Loki 最聪明的一步。

代价也很实在:如果你真的需要“全库全文检索”(比如安全审计、合规查询),Loki 不合适——那是 ES 的场子。又一次:用对工具。

注意:Loki 的标签同样有基数问题**!** 和 Prometheus 一模一样:把 user_id 放进 Loki 的标签,一样会炸(它的索引也是按标签组合建的)。

正确做法:user_id 放进日志内容(JSON 字段),用 | json | user_id="678" 过滤——内容里可以有任意高基数的东西,标签里不行。

这是 Loki 最常见的误用:以为“日志嘛,随便打标签”,结果把 Loki 用成了一个更慢的 ES。基数思维在这里同样适用。

日志分级:一个实用判据

DEBUG/INFO/WARN/ERROR 人人都会写,但标准极其混乱**。给一个可执行的判据:**

级别判据生产开吗
ERROR需要人看(有人得做点什么)✅
WARN可能有问题,但系统自己处理了(重试成功了)✅
INFO描述正常的重要事件(服务启动、配置加载、发布)✅(但要克制)
DEBUG排查时才要的细节❌(按需临时开)

最有用的一条:ERROR 的判据是“需要人看”,不是“发生了异常”。

很多代码里,一个正常的业务拒绝(用户输错密码)被打成 ERROR——于是 error 日志里 99% 是噪音,真正的 ERROR 淹没其中。这和告警疲劳是同一个病:信噪比是公共资源。

判据:如果这条 ERROR 一天出现 10 万次而没人做任何事,它就不是 ERROR。 降成 INFO,或者干脆做成指标(login_failures_total 这种东西天生就该是 counter,不是日志)。

这条推论很重要:很多人用日志记录“发生了多少次 X”——那是指标的活。 日志是给“需要看细节的个案”用的,不是给“数数”用的。用日志数数,你会同时付出日志的高成本和聚合的麻烦。

日志和另外两根的关系

回到第 0 部分的排障流:指标发现 → 追踪定位哪一跳 → 日志看清那一跳的细节。

日志是“最后一公里”——当你已经知道“是订单服务、10:23、那个慢请求”时,日志告诉你它当时到底怎么了(哪个 SQL、什么错误、什么参数)。

所以日志的价值高度依赖于“能被精确定位”:

  • 有 trace_id → 从 trace 一键跳过来,只看这一个请求的日志。这是日志最高效的用法。
  • 没有 trace_id → 你只能“按时间 + 服务”捞,在几万条里翻。效率差一个数量级。

第 0 部分提过的“宽事件”批评在这里显出意义: 如果你的日志是结构化 + 高维度 + 带 trace_id 的,它其实已经很接近“宽事件”了——指标可以从它聚合出来、trace 可以从它关联。三支柱的边界,在实践中本来就是模糊的。

SpanEvent 是另一条路 —— 把事件直接挂在 span 上,天然带着完整的 trace 上下文,不用额外做关联。异常堆栈尤其适合这么记。

小结与下一部分

日志的唯一分水岭是结构化——非结构化把“数据”编码成“散文”,而写日志时脑子里的结构在拼成字符串那刻就丢了,之后所有人只能靠正则猜;结构化是及格线,不是优化。Loki 只索引标签、内容不索引:写入极便宜、按标签查极快(与 Prometheus 倒排索引同源)、全文靠扫但因标签已缩小范围而可行——它赌的是“你总是先用标签缩范围”(排障时你总是从指标或 trace 跳过来的,这假设成立);但 Loki 的标签同样有基数问题——user_id 该放内容不放标签,这是最常见的误用。分级判据:ERROR 是“需要人看”,不是“发生了异常”——一天 10 万次没人管的 ERROR 就不是 ERROR;而“数数”是指标的活,不是日志的(用日志数数会同时付出高成本和聚合的麻烦)。日志是排障的最后一公里,其价值高度依赖能否被精确定位——有 trace_id 一键跳,没有就只能翻几万条;结构化+高维+带 trace_id 的日志已接近“宽事件”,三支柱的边界本就模糊。

数据都齐了、也串起来了。最后一个问题:怎么把它变成人能看懂的东西? 👉 第 8 部分 · Grafana。

参考资料

  • [1] Grafana Loki 官方文档:Labels、LogQL、Best practices(标签基数)。
  • [2] OTel 官方文档:Logs、Logs Data Model;OTel Collector pdata/plog。
  • [3] 关联三大支柱、exemplar 与关联、基数。

本页目录