自建日志组件的最低要求与集成测试清单
· 小村 豪 · 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、轮转、保留、失败时的行为,都是有效的做法。
而这套设计是否真正能够发挥作用,必须通过使用真实文件、真实线程、真实进程的集成测试来验证。与其一开始就把实现做大,不如先把最小组成和最低限度的测试集固定下来,之后再逐步扩展会更顺畅。
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
面向 BtoB 企业的 Google 广告小预算运营 ── 用每月数万日元做出成果的设计与每周操作流程
本文面向希望以每月数万日元预算启动 Google 广告的 BtoB 企业,是一份实务指南。内容整理了平均每日预算与结算上限的运作原理、转化跟踪的搭建、聚焦高意向关键词、否定关键词与搜索字词报告的每周运营方法,以及小预算下的出价策略选择,均以 Google 官方资料为依据。
打造能被地区名搜索到的网站 ── 中小企业的本地SEO实战指南(地区页面与Google商家资料)
用「地区名称+行业」搜索却找不到自家公司的中小企业,适合看这篇。内容涵盖修正本地SEO的先后顺序:整理Google商家资料、统一NAP信息、设计地区页面、网站端的基础建设,以及在Search Console上衡量效果,全部以Google官方信息为依据整理成实务步骤。
Windows 事件日志・ETW 入门 ── 让业务应用程序的日志接入操作系统标准机制
Windows 业务应用程序的日志,是不是只靠文件日志来解决?事件日志与 ETW,是运维人员和操作系统标准工具所能看到的另一层记录。本文整理三种手段的分工方式、.NET 中的写入方法、通过 EventSource 进行的 ETW 埋点、收集与排查的实务做法,以及来源未注册、...
不只是 appsettings.json ── Windows 业务应用的配置管理实务(环境差异化设置・敏感信息・写入位置)
本文从实务角度整理 Windows 业务应用的配置管理,涵盖 appsettings.json 的分层、IConfiguration 与 IOptions/IOptionsSnapshot/IOptionsMonitor 的选用方式、环境差异化设置、可写入配置的存放位置、敏...
常见问题
汇总了咨询这一主题时常见的问题。
- 自建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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。