Perl:JSON Lines 怎样保留数字、缺失值与原始消息
日志进入交换文件后,字段仍须保持原来的含义。耗时为零是一条已完成的观测,耗时缺失表示没有这个观测;消息为空仍然是一条记录。若把三者都写成空字符串,后续查询无法恢复它们的区别。JSON Lines 给每条记录独立的 JSON 文本,但行格式本身不保证类型、字段集合、排序或错误策略,这些都需要由工具定义。
本篇补齐原定课程 28,并作为 00、10、13、25、38、39 的累计数据契约。工程有两个明确入口:bin/logtool.pl 使用旧四字段 TSV,保留原系列实验;bin/logtool 使用五字段输入与 JSONL 交换。两个文件格式分别验证,不能把旧版三条记录的通过当成新版导入已经完成。完整实现位于 lib/LogTool/Course.pm,断言位于 t/course-core.t,命令重放位于 tools/replay.pl。
字段契约先于序列化
五字段顺序为 UTC 时间、等级、服务标识、毫秒耗时、消息。字段之间只有实际 TAB;消息中的 TAB、LF、反斜杠分别用 \t、\n、\\ 表达。空耗时映射到 undef,非空耗时只接受不带符号、不带前导零、最多九位的十进制整数。等级集合为 INFO、WARN、ERROR。服务标识允许 ASCII 字母开头,后面允许字母、数字、下划线和连字符,并保留大小写。
1 | |
代码先以 split /\t/, $line, -1 保留末尾空字段,再要求字段总数恰好为五。没有 -1 的写法会丢掉合法空消息;额外 TAB 会增加字段数,不能把剩余字段随意拼回消息来掩盖错误。时间外形使用 [0-9],随后用 timegm_modern 检查日历日期。形状正确的二月三十日仍然被拒绝。这里没有自动把本地时间转换成 UTC,也不接受任意 ISO 8601 变体。
消息转义由一个完整语法检查约束:普通字符或反斜杠加 t、n、反斜杠。单个尾部反斜杠、\q、未转义控制字符都不合法。先校验再一次替换,避免把 \\n 多次替换成实际换行。消息解码后做 NFC,raw_message 留下规范化前的已解码文本;服务标识不参与 NFC。原始文件和拒绝行是另外的证据,raw_message 并不是源文件全部字节的副本。
[PATTERN] 交换格式只运输已定义的数据含义。先明确缺失、空值和转义,再选择序列化方式;否则读写都成功也可能丢掉语义。
数字与字符串在 JSON 中不同
parse_line 用 0 + $duration 建立数值,空字段直接产生 undef。JSON::PP 编码时,前者成为数字,后者成为 null。消息始终是文本。对零值不能写 if ($duration),因为合法零在 Perl 布尔上下文为假;判断缺失使用 defined。
1 | |
输出的 duration_ms 是 0,message 和 raw_message 都是 ""。把第四字段删成空字段,只有耗时变为 null,记录数量不变。反过来,从外部 JSONL 读入时,"duration_ms":"0" 不能通过类型检查。读入端在对耗时做任何数值转换之前检查它重新编码出的 JSON 标量形态;若先执行加零,再检查类型,数字字符串就已经被程序改造成数字,反例失去意义。
JSON::PP 的 canonical 控制对象键的输出顺序。它不负责记录之间的顺序,也不等于某个外部协议的通用 canonical JSON 标准。这里数组顺序沿用输入,报表等级另行排序;交换文件采用核心模块的这个固定版本和固定选项,其字节散列才有可比较的意义。
[PATTERN] 在边界检查原类型,在内部建立统一类型。类型转换放错位置,会让验证器证明自己已经改过的数据。
一次编码、一次解码
输入通过 '<:raw' 打开,decode('UTF-8', ..., FB_CROAK | LEAVE_SRC) 产生字符。JSON::PP 对象接口在本工具中处理字符文本,不启用 utf8 选项;编码完成后由 Encode::encode 转为 UTF-8 字节。读取交换文件时做相反过程。另一种合法选择是让 JSON::PP 的 utf8 选项直接处理字节,但不能把两种接口混用,重复编码或解码会破坏中文。
JSON 对消息中实际换行产生 \n 转义,因此一条记录仍占交换文件的一条物理行。读入端要求每行是对象,并限定为六个字段:timestamp、level、service、duration_ms、message、raw_message。数组、未知字段、坏 JSON、错误类型都失败。读入后重新检查原消息对应的 NFC 是否等于 message,不能接受调用方随意伪造的规范化结果。
1 | |
运行时须使用版本表冻结的解释器和对应核心模块。本篇代码本身不使用 signatures 或 class。course-core.t 的中文往返、组合字符、转义反斜杠、空消息、数字零、null 和错误类型是独立断言,不能只凭终端显示几个汉字就判定编码链正确。
行边界、坏行与整批失败
TSV 接受 LF、CRLF 和最后一行没有换行。每行最多 65,536 字节,含该行终止符;读入用 4,096 字节块,不先分配一条任意大的行。超长行丢弃到换行,再继续下一条,拒绝列表记录其行号及原因。交换 JSON 的上限为 524,288 字节,因为原消息转义与两个消息字段会扩大输出;两个上限都不等于任意大文件支持。整份输入另受 16 MiB 限制,累计结果保存在内存中。
格式错误进入拒绝列表;非法 UTF-8、I/O 错误和整文件超限终止整批,不发布此次 accepted 文件。这个区分使恢复策略明确:语法错误有行号可回看,输入字节无法正确解释时不能继续猜测。超长行只保留行号和原因,完整原始证据仍是输入文件。
输出先发布拒绝文件,再发布接受文件,每个文件各自使用同目录临时文件、显式 close 与 rename。两个文件不是共同事务;第二次发布失败时,第一个文件可能已更新。读取方必须核对运行状态和这一批两个产物,不能把任何已存在文件都当成成功批次。协作锁不约束不合作写入者,rename 也不能证明断电持久化。
fixtures/course.tsv 有五条输入:四条合法记录和一条坏时间。重放要求 INPUT=5、ACCEPTED=4、REJECTED=1,其和必须守恒;报表固定为 TOTAL 4、ERROR 1、INFO 2、WARN 1。查询最低耗时 0 返回三条,缺耗时的 null 不满足数值条件。查询字面量 .* 只返回包含这两个字符的 ERROR 行,不能解释为任意文本模式。
练习:把 JSONL 的数值零改成字符串零,为什么仍然可以被 JSON 语法解析,却不能通过本工具?JSON 语法允许两者;课程数据契约要求耗时为数字或 null。另一题:将服务名 API 改为 api,NFC 会把它改回吗?不会,服务标识没有大小写折叠,也不参与消息规范化。
| 决策 | 可核对的边界 |
|---|---|
| 缺失耗时 | JSON null,不等于数字零 |
| 空消息 | 合法记录,不减少计数 |
| 规范化 | 只变换 message,保留 raw_message |
| 语法错误 | 拒绝列表;与整批字节/I/O 失败分开 |
| 输出发布 | 单文件替换;两文件不是共同事务 |
参考:Perl 5.38.2 JSON::PP、Encode、perluniintro。实现、金标和本次结果以工程及 writing-plans/perl-language/completion/evidence/ 为准。






