Node.js 实战:零依赖解析 zstd 压缩的 JSONL 会话日志

举报
yd_236554214 发表于 2026/10/11 11:10:37 2026/10/11
【摘要】 Node.js 实战:零依赖解析 zstd 压缩的 JSONL 会话日志一次真实项目踩坑记录:如何在 Node.js 中不引入第三方库,直接解析 .zstd 压缩的 JSONL 流式日志,并完成 token 用量审计。 一、背景应用每天会在 ~/.dsh/sessions/ 下生成会话日志,每条会话一个目录,内含 session.jsonl.zstd。文件是 zstd 压缩的 JSONL(...

Node.js 实战:零依赖解析 zstd 压缩的 JSONL 会话日志

一次真实项目踩坑记录:如何在 Node.js 中不引入第三方库,直接解析 .zstd 压缩的 JSONL 流式日志,并完成 token 用量审计。

一、背景

应用每天会在 ~/.dsh/sessions/ 下生成会话日志,每条会话一个目录,内含 session.jsonl.zstd。文件是 zstd 压缩的 JSONL(每行一个 JSON 事件),包含 type、seq、time 和 data.usage 等字段。我需要写一个审计脚本,统计输入/输出 token、cache 命中量,并排查父子会话 token 重复计数的问题。

二、关键技术点:zstd 魔数定位 + 逐帧解码

Node.js 原生 node:zlib 提供了 zstdDecompressSync,但直接对整个文件解压经常失败——因为日志是追加写入的压缩流,可能存在"撕裂帧"(写入到一半的残帧)。解决方案是按帧处理:

zstd 帧的魔数是 28 B5 2F FD,用字节扫描定位每个帧的边界;
对每个完整帧单独调用 zstdDecompressSync 解码并拼接;
遇到解码到 EOF 的残帧,直接跳过(committed-prefix 语义,容忍撕裂帧)。

const zlib = require(‘node:zlib’);
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);

function zstdFrames(buf) {
const frames = [];
let off = 0;
while (off < buf.length) {
const i = buf.indexOf(MAGIC, off);
if (i === -1) break;
// 找下一帧起始,若没有则到文件尾
const next = buf.indexOf(MAGIC, i + 4);
frames.push(buf.subarray(i, next === -1 ? buf.length : next));
off = next === -1 ? buf.length : next;
}
return frames;
}

function decodeAll(buf) {
let out = ‘’;
for (const frame of zstdFrames(buf)) {
try {
out += zlib.zstdDecompressSync(frame).toString(‘utf8’);
} catch {
// 残帧(撕裂帧)直接跳过,不中断整体流程
}
}
return out;
}

三、审计模型:token 四分类与父子会话去重

日志中每个事件的 data.usage 把 token 分为四类:input、output、cacheRead、cacheWrite。统计时按 provider/model 聚合即可得到全局用量。

最棘手的问题是父子会话继承:子会话会携带父会话的前缀 seedLength(上下文种子长度),导致同一批前缀 token 被父子两个会话各统计一次,出现重复计数。解决方案是:

读取 session 事件中的 parentSession 与 seedLength 元信息;
判断子会话事件签名是否与父会话前缀一致(前缀继承判定);
对继承的部分只在祖先会话中计一次。

function dedupePrefix(child, parent, seedLength) {
// 事件签名前缀与父会话一致 => 继承上下文,不计入子会话
const sig = child.map(e => e.type + ‘:’ + e.seq).slice(0, seedLength);
const pSig = parent.map(e => e.type + ‘:’ + e.seq).slice(0, seedLength);
return JSON.stringify(sig) === JSON.stringify(pSig);
}

四、交叉对账:统计的可审计性

为了不"自己骗自己",我把从原始日志重算的 token 与既有 usage-stats/index-v1.json 索引做双向对账,输出 OVER/UNDER 差异百分比。任何一处差超过阈值就告警,从而把"统计结果"变成"可审计的统计结果"。

const diff = (recomputed - index) / index * 100;
if (Math.abs(diff) > 5) {
console.warn(${model} UNDERCOUNT${diff.toFixed(2)}%);
}

五、工程复盘:值得保留的三个习惯

探索式逐层验证:先 peek 格式 → 再 shape 分布 → 再解码深挖,每步打印中间结果,确认后再写下一步,极大降低返工概率;
容错是第一优先级:所有解码与 JSON 解析都用 try/catch 包裹,坏帧单独归类计数,绝不让单条脏数据拖垮整个流水线;
结果必须可复核:统计类脚本一定要留一条"重算 + 对账"的路径,输出差异率,而不是直接给出一个无法验证的数字。
六、结语
zstd + JSONL 是现代日志系统的双料黄金组合:压缩率高、可流式追加。配合 Node.js 零依赖的帧级解码方案,轻量脚本即可完成生产级审计任务。希望这篇文章对你有帮助。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。