jq 处理 JSON 日志的常用招式:嵌套取值、流式统计与大整数精度
排查一个慢接口,我手上是 2.3GB 的 JSON 日志,一行一个对象。第一次我用 jq -s 想一把算完,机器直接 OOM——-s 会把整个文件读进内存拼成一个数组。改成流式写法后,同样的统计 47 秒跑完,内存稳定在 30MB 出头。
一、先按 JSONL 读对
jq 默认逐行读取,天然适合一行一个 JSON 的日志,压根不需要 -s:
jq -r '.level' app.jsonl | sort | uniq -c | sort -rn | head
jq -r 'select(.level=="ERROR") | .path' app.jsonl | sort | uniq -c | sort -rn | head -n 5
-r 去掉输出里的引号,不加的话 uniq -c 会把带引号和不带引号当成两种值。select 放在管道最前面,jq 解析完立刻过滤,不为丢弃的行做额外构造。
二、-s 什么时候真的会炸
jq -s 把全部输入拼成一个数组,内存通常是文件大小的 3 到 10 倍(解析后的结构比文本更占地方)。2.3GB 的文件,-s 至少要 7GB 以上,小规格云主机上必然被 OOM killer 干掉。先量三行再决定:
ls -lh app.jsonl
free -m | head -n 2
wc -l app.jsonl
文件大于可用内存的一半,就别考虑 -s。必须全局聚合时,用 reduce 配 inputs 流式累加,内存只跟结果集大小有关:
jq -n 'reduce inputs as $x ({}; .[$x.level] = ((.[$x.level] // 0) + 1))' app.jsonl
-n 表示不拿第一个输入当初始值,inputs 逐个吐出剩下的对象。这段只维护一个计数对象,跑 2.3GB 时 RSS 峰值 30MB 左右。
三、嵌套取值与默认值
日志字段经常缺,.user.id 取不到就是 null,拼进字符串就变成字面的 null:
jq -r '[.ts, (.user.id // "-"), (.req.path // "-"), (.dur|tostring)] | @tsv' app.jsonl | head -n 3
三个要点:// 只在左边为 null 或 false 时取右边,0 和空字符串不会被替换;@tsv 会转义制表符,比自己拼 + "\t" + 稳;.dur|tostring 防止数字混进字符串列,否则后面 sort -n 直接失效。中间层不是对象时会报 Cannot index number with "b",加 ? 就变 null:.a?.b? // "-"。
四、别急着 group_by
group_by 内部要先排序,本质仍要拿到全部数据。逐行读的场景下更划算的是先投影,再交给系统 sort:
jq -r 'select(.level=="ERROR") | [.svc, .code] | @tsv' app.jsonl \
| sort | uniq -c | sort -rn | head -n 10
同一份 2.3GB 日志,jq -s 'group_by(.svc)' 跑了 11 分钟还被 OOM 杀过两次;投影加 sort 版 47 秒出结果。差在 sort 会用外部归并,吃内存少得多。
按时间窗聚合,先把时间转成秒再整除:
jq -r '(.ts|fromdateiso8601/60|floor) as $m | "\($m) \(.code)"' app.jsonl \
| sort | uniq -c | sort -k2,2n | head
五、混杂日志:跳过不是 JSON 的行
生产日志里常混着启动横幅、堆栈和空行,直接 jq 会报 parse error 并中断整条管道:
jq -Rr 'fromjson? | .level' app.jsonl | sort | uniq -c | sort -rn
-R 按原始文本逐行读,fromjson? 解析失败返回空,管道自然跳过,所以后面接 .level 是安全的。想知道有多少行解析不了:jq -R 'fromjson? // empty' app.jsonl | wc -l,再和总行数一比就有数了。
六、大整数会被悄悄改掉
jq 1.6 及更早用双精度浮点存数字,超过 2 的 53 次方的 ID 会丢精度:19 位的订单号读出来末尾几位直接变了,而且不报任何错,写回下游就是脏数据。
echo '{"id":1234567890123456789}' | jq -r '.id|tostring'
jq --version
jq 1.7 起保留数字的原始字面量,同样的输入能原样输出。升级不了就让上游把 ID 序列化成字符串,或者用 grep -o 单独抠出来,别让它经过 jq。
七、几条常备招式
-c 输出紧凑单行,喂给别的工具必备;-e 用退出码表达判断,jq -e '.ok' 在 false 或 null 时返回 1,可以直接写进 if;--arg 和 --argjson 传外部值,别把变量拼进查询字符串。最后一条示例把慢请求挑出来:
jq -e --arg svc api 'select(.svc==$svc and .dur>1000) | .ts' app.jsonl \
> /tmp/slow.txt && echo "有慢请求"
一句话结论:动手前先问一句"这份数据能不能一次读完"。能,-s 随便用;不能,就逐行投影加 sort | uniq -c,或者 reduce inputs 流式累加。我踩过的两个坑都藏在这里:-s 的 OOM 和 2 的 53 次方的精度丢失,它们都不报错,只给你一个错的答案。
- 点赞
- 收藏
- 关注作者
评论(0)