自建日志组件的最低要求与集成测试清单
· 更新日期: · 小村 豪 · Windows Development, Logging, Integration Testing, Test Design, Reliability
更新记录(仅首版,2026年04月02日 发布)
- 首次发布
引用本文(DOI: 10.5281/zenodo.21615551)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《自建日志组件的最低要求与集成测试清单》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615551 https://comcomponent.com/zh-CN/blog/2026/04/02/001-custom-logger-minimum-requirements-and-integration-test-checklist/
- DOI(最新版本)
- 10.5281/zenodo.21615551
- DOI(此版本)
- 10.5281/zenodo.21615552
如果能够使用现成的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、轮转、保留、失败时的行为,都是有效的做法。
而这套设计是否真正能够发挥作用,必须通过使用真实文件、真实线程、真实进程的集成测试来验证。与其一开始就把实现做大,不如先把最小组成和最低限度的测试集固定下来,之后再逐步扩展会更顺畅。
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
信息处理安全保障支援士(情報処理安全確保支援士) 2024年春季(令和6年) 午后问1解说 ── JWT的alg=none与API授权、WAF的临时应对
以信息处理安全保障支援士考试2024年春季(令和6年)午后问1为题材,解说JWT的alg=none、API授权、Mass Assignment(批量赋值)、4位认证码的暴力破解,以及WAF的临时应对措施。
Windows的「内存使用量」究竟表示什么 ── 正确解读 Working Set・Private Bytes・Commit・页面文件
任务管理器中的内存、Working Set、Private Bytes、Commit并不是同一个值。本文讲解Windows虚拟内存与物理内存的关系、页面文件的作用,以及在排查内存不足或泄漏时应该关注的指标。
多线程实务最佳实践 Java 篇 ── 虚拟线程时代的准则
Java 多线程的惯例是不直接创建线程,而是交给 ExecutorService 与虚拟线程来处理。本文整理 synchronized 与 ReentrantLock 的取舍、通过中断实现的协作式停止、ConcurrentHashMap 的原子操作,直至 Swing 的 E...
多线程实务最佳实践 C 语言篇 ── 以 Win32 API 的方式安全编写
C 语言 × Win32 的多线程有其定式:用 _beginthreadex 创建线程、SRW 锁与条件变量、Interlocked、以停止事件 + WaitForMultipleObjects 设计停止流程。本文还将梳理 TerminateThread 的危险性与 Dll...
多线程实务最佳实践 C++ 篇 ── 用 RAII 和 jthread 从结构上消除事故
C++ 的多线程是数据竞争会变成未定义行为的世界。本文梳理 std::thread 析构函数的陷阱、jthread 与 stop_token 的停止设计、scoped_lock 的死锁规避、atomic 的正确定位,直至与 Win32 同步 API 的使用区分。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
技术咨询 & 设计评审
协助梳理设计方向、架构边界、生命周期责任,以及既有 Windows 资产的处理方式。
常见问题
汇总了咨询这一主题时常见的问题。
- 自建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状态。