自建日志组件的最低要求与集成测试清单
· 更新日期: · 小村 豪 · Windows Development, Logging, Integration Testing, Test Design, Reliability
如果能够使用现成的logging框架,那当然更安全。不过,由于应用层面的限制或运维上的现实情况,有时依然无法避免自行实现logger。这时最先让人犯难的是,到底要实现到什么程度,才能做到”既不过于粗糙,又不过于沉重”的设计。
本文把对象范围限定在用于故障排查的应用程序日志上。不一次性背负审计追踪、分布式追踪、指标平台、云端聚合等需求,而是先定义现场真正用得上的最小组合,再整理为了让这套组合真正值得信赴、需要覆盖哪些集成测试要点。
先说结论
第一版应该守住的要点如下。
- 格式采用
UTF-8的JSON Lines - 保持一行一条记录不被破坏
- 必需字段为
时间戳、level、category、message、结构化fields、sessionId、processId - 基本原则是
一个进程对应一个文件 - 负载较低时采用同步写入,负载较高时采用
single writer + bounded queue Error/Critical以及会话的开始与结束要进行同步flush- 轮转与保留从v1阶段就要纳入
- 存储位置不可用时,不要悄悄改写到其他位置
收敛到这个程度,在实现和运维两方面都不容易崩溃。
首先缩小对象范围
自建logger容易变得棘手,多半是因为一开始就想把所有需求都揽进来。如果想用同一套机制一次性覆盖诊断日志、审计日志、性能测量、分布式追踪、用户行为分析,需求会一下子膨胀起来。
这里讨论的对象,是用于排查应用故障的诊断日志。也就是优先保证事后能够追溯”何时”、”在哪个处理环节”、”发生了什么”、”当时处于怎样的上下文”。仅仅把范围缩小到这里,第一阶段的设计判断就会轻松很多。
最低限度需要满足的要求
1. 格式采用UTF-8 JSON Lines
即使用纯文本拼接的方式,也能保留日志,但之后很难以程序化的方式处理。反过来,如果一开始就采用沉重的自定义二进制格式,运维时的可观测性又会下降。
作为折中方案,UTF-8的JSON Lines比较容易处理。只要一行对应一条记录,作为文本阅读也很直观,之后用脚本或工具解析也很方便。即使写入过程中被截断,也容易分辨出损坏的是哪一行,这一点非常适合实务场景。
2. 先固定必需字段
至少应该具备的字段是以下这7个。
timestamplevelcategorymessagefieldssessionIdprocessId
如果只把日志做成message这一个字符串字段,之后搜索条件一旦增多就会很麻烦。反过来,字段加得太多,又会让调用方的负担骤增。最初先固定在这个范围,等真正需要时再考虑扩展会更稳妥。
3. 以”一个进程一个文件”为基本原则
让多个进程向同一个文件追加写入的设计,事故隐患远比看起来的多。因为互斥控制、部分写入、轮转时机、异常终止时的处理会一下子变得棘手。
请先以一个进程一个文件为基本原则。如果想把多个进程的日志汇总起来,应该在后段进行聚合,或者明确建立一个专门的聚合进程,这样更安全。
4. 写入策略按负载区分
日志量较少的阶段,同步写入更直观,也更便于排查故障。强行改为异步化,反而容易丢失结束前的日志,或让异常发生时的flush条件变得模糊。
另一方面,当日志量增大、同步I/O成为瓶颈时,就应该采用single writer + bounded queue。这时重要的是提前决定队列溢出时的策略。不要把丢弃旧日志、丢弃新日志、还是发出警告这个问题留在模糊地带。
5. 确定flush条件
Error和Critical,以及会话开始与结束的日志,采用同步flush会在故障排查时很有帮助。但如果连日常的Info日志也全部flush,就会拖慢速度,因此不把所有级别一视同仁才是现实的做法。
6. 轮转与保留从v1阶段就要纳入
轮转常被认为”以后再加也来得及”,但一旦进入运维阶段,就会突然变得棘手。按大小、按天、按每次启动等方式都可以,但至少要确保”不会无限增长”以及”保留几份”这两点是明确的。
7. 存储失败时不要擅自改写到其他位置
当日志的存储位置不可用时,悄悄改写到其他地方的设计,会让日后的排查变得困难。运维人员在”应该存在的位置”找不到日志,故障应对的初期动作就会延迟。
如果无法保存,就应该通过应用内通知、事件日志、标准错误输出等明确可辨识的手段,把失败暴露出来。至少要避免出现”不知道日志跑去哪里了”这种状况。
v1最小组成的大致印象
第一版往往做到以下程度就足够了。
UTF-8 JSON Lines- 一个进程一个文件
- 以会话为单位的文件名
- 基于大小或每次启动的轮转
- 保留份数的上限
Error/Critical的同步flush- 能够接收结构化
fields的接口
超出这些范围的功能,等实际运维中真正遇到困扰之后再添加,反而更容易长期维护。
常见的反面例子
以下也列举一些应当避免的典型做法。
- 把所有内容都塞进
message字符串 - 多个进程共享同一个文件
- 不确定flush条件就全面改为异步化
- 把轮转和保留往后拖延
- 存储失败时悄悄改写到其他目录
- 把网络传输或本地数据库存储一次性都塞进v1
这些做法乍看都很方便,但都容易让排查和运维变得更沉重。
集成测试应基于真实文件、真实线程、真实进程来考虑
logger是仅靠单元测试难以让人安心的组件。因为只验证字符串格式化和JSON化,无法捕捉到实际运维中才会暴露的I/O、并发、轮转、结束时flush、权限错误等问题。
因此,集成测试需要使用真实文件、真实线程,必要时还要用真实进程来确认。至少要避免出现”平时都能通过,但故障时不可信”这种状态。
应该覆盖的集成测试项目
单次写入的健全性
- 一行是否对应一条JSON记录
- 是否能以
UTF-8重新读取 - 必需字段是否每次都齐全
- 是否因换行混入而破坏成多行
同一进程内的并发执行
- 多个线程同时写入时记录是否会损坏
- 记录数量是否存在多余或缺失
- 使用队列时,顺序与丢弃策略是否符合规格
flush与结束时的行为
Error/Critical是否立即生效- 正常结束时队列内是否已清空
- 在接近异常终止的路径下,必要的结束日志是否仍能保留
轮转与保留
- 达到轮转条件时能否切换到新文件
- 超过保留上限的旧文件是否按规格被删除
- 轮转前后JSON行是否不会损坏
异常场景
- 存储目录不存在时的处理
- 没有写入权限时的处理
- 磁盘已满等场景下失败时的通知或返回值
- 队列溢出时的行为
多进程场景的处理
如果规格是一个进程一个文件,那么”其他进程不会试图写入同一个文件”这件事本身,就可以作为验证对象。反之,如果采用聚合进程的方式,则还需要连同转交失败的场景一起验证。
v1阶段至少要通过的测试数量
一开始就想全部覆盖,会让测试变得过于沉重。v1阶段至少应该通过的,大致是以下这6项。
- 单线程下的正常写入
- 多线程同时写入
Error/Critical的flush- 轮转与保留
- 存储位置异常时的失败通知
- 正常结束时的drain与最终flush
只要这6项测试通过,就能明显摆脱”字符串确实输出了,但运维时不可信”的logger状态。
总结
自建logger的第一个目标,不是功能丰富,而是”故障时可以被信赖”。为此,把格式固定为UTF-8 JSON Lines、收窄必需字段、以一个进程一个文件为基本原则,并提早确定flush、轮转、保留、失败时的行为,都是有效的做法。
而这套设计是否真正能够发挥作用,必须通过使用真实文件、真实线程、真实进程的集成测试来验证。与其一开始就把实现做大,不如先把最小组成和最低限度的测试集固定下来,之后再逐步扩展会更顺畅。
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
什么是数字发票?──与「用邮件发送 PDF 发票」有何不同
数字发票是一种让发票信息从卖方系统直接对接到买方系统、无需人工介入的机制。本文将通俗易懂地解析数字发票与 PDF 发票的区别、它与日本 Invoice 制度的关系、Peppol 与 JP PINT 的运作方式、与电子账簿保存法的关系,以及中小企业该如何入手。
Windows 应用的任务栏托盘常驻与 Toast 通知 —— NotifyIcon 的坑与 AppNotification 的选型
本文整理了将业务 Windows 应用常驻在任务栏托盘(通知区域)并通过 Toast 通知告知用户的实现要点。内容涵盖 NotifyIcon 的正确用法与「关闭后驻留托盘」的设计、资源管理器重启后的重新注册、三种 Toast API(Windows App SDK AppN...
从 WordPress 迁移到 Movable Type ── 正因为是「反方向」,才更需要梳理清楚的实务步骤
从实务角度解说从 WordPress 迁移到 Movable Type(MovableType.net)的步骤。整理迁移合理的场景、文章与固定页面及图片的导入、自定义文章类型的处理方式、通过 URL 设计与 301 重定向延续 SEO 效果,以及插件功能的替代方案。
VB6 应用能用到什么时候 ── 运行时支持现状与务实的 .NET 迁移做法
VB6 应用程序到底能用到什么时候?本文整理 VB6 运行时的支持政策(Windows 11 也在支持范围内)与 IDE 支持早已终止这一不对称现状,并以实务指南的形式说明全面重写、自动转换、分阶段迁移的判断表、迁移前的资产盘点、VB6 与 .NET 的不兼容之处,以及 C...
把传真订单迁移到 Web ── 双轨运行期的设计与分阶段迁移实务
介绍将传真订单迁移到 Web 订单或 CSV 导入的实务做法。整理一次性全面 Web 化容易失败的原因、传真与 Web 双轨运行期的设计、商品与客户主数据的整备、CSV 导入这一中间形态,以及如何让客户配合的分阶段迁移步骤。
常见问题
汇总了咨询这一主题时常见的问题。
- 自建logger的日志格式应该选什么?
- 推荐使用UTF-8的JSON Lines,保持一行一条记录不被破坏的格式。纯文本拼接后续难以用程序化方式处理,而自定义的二进制格式又会降低运维时的可观测性。JSON Lines既能以文本方式阅读,也便于脚本和工具解析,即使写入过程中被截断,也容易分辨出哪一行损坏,因此更适合实务使用。必需字段固定为timestamp、level、category、message、结构化fields、sessionId、processId这7项。
- 日志写入应该用同步还是异步?
- 应按负载区分。日志量较少的阶段,同步写入更直观,也更便于排查故障。强行改为异步化,反而容易丢失结束前的日志,或让异常发生时的flush条件变得模糊。当日志量增大、同步I/O成为瓶颈时,可以采用single writer + bounded queue的方案,并且要提前决定队列溢出时的策略:是丢弃旧日志、丢弃新日志,还是发出警告。Error/Critical以及会话开始与结束的日志建议采用同步flush,这样在故障排查时会很有帮助。
- 可以让多个进程写入同一个日志文件吗?
- 应该避免。让多个进程向同一个文件追加写入的设计,会让互斥控制、部分写入、轮转时机、异常终止时的处理一下子变得棘手,事故隐患远比看起来的多。基本原则是一个进程对应一个文件,如果想把多个进程的日志汇总起来,应该在后段进行聚合,或者明确建立一个专门的聚合进程,这样更安全。
- 自建logger的集成测试应该确认哪些内容?
- 需要用真实文件、真实线程、真实进程来验证。仅靠字符串格式化和JSON化的单元测试,无法捕捉到实际运维中才会暴露的I/O、并发、轮转、结束时flush、权限错误等问题。v1阶段至少要通过6项测试:单线程下的正常写入、多线程同时写入、Error/Critical的flush、轮转与保留、存储位置异常时的失败通知,以及正常结束时的drain与最终flush。只要这6项通过,就能明显摆脱「运维时不可信」的logger状态。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。