2.11 一个著名的日志系统是怎么设计出来的
从模块各自拼接输出走到事件、Logger 层级、级别过滤、布局和 Appender 正交组合,沿证据链设计可诊断的日志系统。
学习目标
- 能沿事件、Logger 层级、级别过滤、布局和 Appender 追踪一条日志记录
- 能解释日志系统为什么把调用点、过滤规则、输出格式和输出目的地拆开,并保留请求上下文
- 能在正常、边界和故障场景中回答:禁用级别时怎样避免昂贵消息构造,输出端失败时怎样保留诊断线索
2.11 一个著名的日志系统是怎么设计出来的
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.11 一个著名的日志系统是怎么设计出来的。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
先想象一座工厂的值班室:每个工位都把纸条交给同一个登记台,登记台决定哪些纸条值得保留、用什么格式抄写、送到哪几个档案柜。若工位自己决定文件名和格式,换一台机器就要重写所有工位;若登记台没有身份和时间线索,纸条又无法用于追查事故。
这一章解决的是“业务代码怎样记录事实,同时不被输出策略绑死”。没有统一边界,日志会重复拼接、敏感字段失控、禁用级别仍浪费 CPU,故障时还会因为输出端阻塞拖垮业务。验收要同时看事件内容、过滤决定、格式结果、目的地状态和上下文关联。
三个会让日志系统失真的陷阱
七个目录节点到日志证据
2.11 一个著名的日志系统是怎么设计出来的
总合同是:调用点提交结构化事实,Logger 层级选择继承的配置,级别过滤决定是否继续,布局只负责表示,Appender 负责交付。每条关键日志至少要能关联事件名、时间、级别、Logger 名称、请求上下文和输出结果。
前言
↡从各模块自行写文件、拼接文本,演进到由统一日志管线集中处理事件和输出策略的问题背景。不是一段产品宣传,而是边界设计的起点:先列出调用点必须提供的事实,再把过滤、格式和目的地留给系统。这样才可以在不改业务代码的情况下切换控制台、文件和集中采集端。
张家村
↡用故事化场景说明分散记录方式会造成重复配置、格式不一和故障难查的阶段。的价值在于提出约束:不同模块要共享级别、上下文和输出策略,但不能共享可变的业务状态。复核时观察同一个请求跨模块的日志是否拥有同一 trace ID,以及禁用级别时是否真的停止了消息构造。
小张的设计
↡把日志事件、Logger 选择、级别过滤、布局格式和输出目的地分别建模的组合设计。把一个“写字符串”动作拆成五个可替换阶段。阶段之间传递事件和上下文,不让布局反过来修改业务字段;每个阶段都有可测试的正常、边界和失败结果。
正交性
↡让级别、格式、目的地和 Logger 层级彼此独立组合,改变一个维度时不必复制另一个维度配置的设计性质。的验收不是“配置文件很长”,而是只把文本布局换成 JSON,过滤和目的地仍保持原语义;只把某个包的级别调高,其他包也不应被意外静音。
Log4j
↡Java 生态中用于记录事件的日志框架家族,本页把它作为 Logger、级别、布局和 Appender 组合思想的技术参照。是技术参照,不是把某个版本 API 当成业务合同。写设计时保留稳定的事件模型,把具体框架的配置键、桥接器、异步队列和版本差异隔离在适配层,并验证其实际过滤与输出语义。
尾声
↡从日志系统回到工程结论:可观测信息必须可关联、可过滤、可交付,也必须有容量和隐私边界。提醒我们,更多日志不等于更多诊断。系统要明确采样、保留期、字段脱敏、失败降级和成本预算;对关键业务事实,日志还应能与指标、追踪和审计记录互相印证。
最小结构化日志合同
void recordOrder(String traceId, String orderId, int itemCount) {
if (!logger.isInfoEnabled()) return;
logger.info("order.created trace={} order={} items={}",
traceId, redact(orderId), itemCount);
}合同把过滤判断放在昂贵工作之前,并让布局层决定最终表示。真实系统还要定义 trace ID 的来源、订单号脱敏规则、参数类型、时钟精度和 Appender 失败时的降级;禁止把密码、令牌或完整个人信息当成调试便利写入。
五步复核一条日志管线
1. 创建事件并固定上下文
先记录事件名、级别、Logger 名、时间、trace ID 和脱敏字段。基线使用一个订单事件;只改变一个条件,例如把字段改成超长值,预期是截断/拒绝策略明确而不是布局无限增长。
Lab
日志级别与 Appender 实验
只改变过滤或目的地状态,观察消息构造成本、输出结果和故障降级如何变化。
INFO 开启,结构化事件交给本地 Appender
order.created + trace-17 → threshold pass → JSON → file append
判定
accept:字段可检索,业务线程只提交一次事件
当前样本:正常记录;保存事件字段、有效级别、构造计数、等待上限和兜底输出。
正常、边界与故障证据矩阵
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 级别开启、字段合法、Appender 可用 | 事件被格式化并交付,trace ID 保持 | 事件字段、有效配置、输出片段 |
| 边界 | 级别关闭、字段超长或子 Logger 覆盖 | 不做无效计算,截断/拒绝规则可解释 | 构造计数、配置快照、拒绝原因 |
| 故障 | 文件/网络 Appender 超时或不可写 | 按策略降级、计数告警,不阻塞业务 | 首个故障、等待时长、兜底输出 |
故障诊断:先问事实在哪一段消失
- 事件侧:查事件名、级别、Logger 名、时间、trace ID 和脱敏;缺上下文时先修调用合同。
- 配置侧:查层级继承、运行时覆盖和有效阈值;不能只看配置文件,必须保存最终生效快照。
- 过滤侧:查级别判断是否早于序列化和昂贵参数;关闭级别时用计数器证明计算没有发生。
- 交付侧:查布局、编码、缓冲、超时、重试和丢弃;Appender 失败要与业务错误分开记录。
如果没有任何日志,先区分事件没创建、Logger 被过滤还是 Appender 没交付;如果只有部分字段,查布局和脱敏;如果业务变慢,查被过滤事件是否仍在做昂贵计算以及输出端是否同步阻塞。每次只改变一个配置维度,并保留恢复后的基线。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 前言
从各模块自己输出走向统一日志管线时需要先定义的边界问题。
- 张家村
用故事呈现分散记录导致重复配置、格式混乱和故障难查的场景。
- 小张的设计
将事件、Logger、过滤、布局和输出目的地拆开的组合方案。
- 正交性
改变级别、格式或目的地中的一个维度,不破坏其他维度的语义。
- Log4j
Java 生态中的日志框架家族,本页用它参照 Logger 与 Appender 的组合设计。
- 尾声
日志设计回到可关联、可过滤、可交付且受容量与隐私约束的工程结论。
练习
练习
问题 1: INFO 级别关闭时,为什么仍可能造成明显 CPU 消耗?如何改进调用点?
问题 2: 只把文本布局换成 JSON,怎样证明系统满足正交性?
问题 3: 修改本页“日志级别与 Appender 实验”的故障场景,使网络输出端超时,并说明业务线程和诊断证据应如何恢复。
本页小结
- 调用点提交事实,不决定过滤、布局和目的地。
- Logger 层级与级别过滤决定事件是否继续流动。
- 正交布局和 Appender 让输出策略可替换、可降级。
- trace ID、脱敏和失败证据让日志真正可诊断。
读完后的自测问题是:当 INFO 被关闭且网络 Appender 超时时,你能否分别证明昂贵消息没有构造、业务线程没有无限等待、关键诊断仍可沿 trace ID 找回?