当解析器悄悄什么也没取到
一句话总结
Loki 不为正文建立索引,所以要用正文里的值来过滤,就必须在每次查询时用解析器把它取出来。解析器出错时会加上 __error__ 标签,但有的解析器什么也取不出来时也不报错。
为什么需要它
有个团队在用 Loki 衡量支付服务的错误率。查询是 {app="pay"} | logfmt | status="500",仪表板平静地过了几个月。然而在故障会议上,数了数同一时段实际的 5xx 数量,是仪表板上数字的三倍。
原因很简单。那个服务的一部分路径是以 JSON 打印日志的。logfmt 解析器遇到 JSON 行时不会报错。它只是什么标签也取不出来就过去了。没有标签,status="500" 就为假,那一行就悄悄漏掉了。仪表板上既不会显示“无数据”,也不会显示“解析错误”。只是数字变小了而已。
工作原理
LogQL 的解析器有四种。全都接在选择器和行过滤器后面,每次查询都会重新读取这些行来生成标签。
| 解析器 | 读取的格式 | 失败时 |
|---|---|---|
logfmt |
키=값(占位符为键和值)用空格连起来的行 |
不报错,什么也取不出来 |
json |
一行 JSON 对象 | 加上 __error__="JSONParserErr" |
pattern |
提取位置用 <이름>(占位符为名称)标出的固定形态 |
形态不同就取不出来 |
regexp |
带有命名捕获组的 RE2 | 不匹配就取不出来 |
所以调查总要分两路进行。先用 | json | __error__!="" 数一数有多少行是坏的,再用 | logfmt | status="" 数一数没有报错但值没出来的行有多少。两个数字都为 0,才能相信那个流的解析器。
想删掉解析出错的行,就加上 | __error__=""。但不能习惯性地加——那些行正是“混进了格式不同的日志”的信号。先数,查清原因,然后再删。
提取出来的标签只在该次查询内存在。既没有被存储,也没有被索引。所以改变解析器,过去的数据也会被一并重新解释——这与指标正好相反。指标如果埋点错了,过去就永远消失了,而日志只要正文还在,以后可以用另一种眼光重新读一遍。这就是“不建立索引”这个选择带来的最大好处。
值始终是字符串。像 | dur_ms > 500 这样比较时,LogQL 会把它转成数字,但如果单位混在一起(毫秒和秒在同一个字段里),就会悄悄得出错误的答案。装数字的字段,最好把单位写进名字里。
在现场相遇的样子
最常见的事故是“一个流里有两种格式”。应用以 JSON 打印,但它前面的代理或运行时把纯文本警告混进了同一个 stdout。流标签相同,所以是一个流,而查询只能用一个解析器。
解法不在查询,而在布局。格式不同的日志要换一个标签,送到另一个流里。在采集器里分一次,查询就简单了,解析器错误也会消失。如果做不到(遗留系统),至少要做一个按解析器分别计数再合并的查询,并把这个事实写在仪表板旁边。
还有一点。pattern 解析器比 regexp 更快、更易读,对访问日志这种位置固定的行,几乎总是更好。正则表达式只用于位置会晃动的行。
下一项实验要做什么
把一个混有三种格式的流和一个格式整洁的流放进 Pod 里的真正的 Loki,依次接上四个解析器。分别数出 json 报错的行数和 logfmt 悄悄放过的行数,用 pattern 和 regexp 从遗留行里取出值,最后求出必须按格式分别计数再相加才能得到的真正的 5xx 数量。