手把手配置日志解析规则:JSON 自动解析、正则/grok 字段提取、多行堆栈合并、时间与类型处理、敏感字段脱敏,以及调试解析规则和解析失败率的实用方法。
原始日志就是一行字符串。如果直接全文检索,查「支付失败」能搜到,但查「哪个用户、哪个订单、失败码是什么、耗时多少」这类问题就抓瞎——因为这些信息藏在字符串里,检索引擎分不出来。结构化的本质,是把一行日志拆成一个个有名字的字段(user_id=123、status=fail、cost_ms=450),之后才能做过滤、聚合、告警和关联。
OBSERVE 的采集 Agent 在入库前会经过一条解析管道,把原始日志变成结构化字段。这条管道的配置,就是本文要讲的重点。
JSON 自动解析。如果你的应用直接输出 JSON 日志(强烈建议),几乎不用写规则。配置里加一个 json 解析器,Agent 会把 JSON 字段自动展开成可检索字段:
parsers:
- type: json
field: message
要求是整行就是合法 JSON。嵌套对象会被展平成 a.b.c 形式,数组会拆成多行。缺点是不能处理「JSON 里混了非 JSON 文本」的情况,那种要用正则。
正则提取(grok 风格)。对非结构化日志,用命名捕获组把字段抠出来。以一条 Nginx 访问日志为例:
log_format: '$remote_addr - $user [$time_local] "$request" $status $body_bytes_sent'
对应的解析规则:
parsers:
- type: regex
pattern: '^(?P<remote_addr>\S+) - (?P<user>\S+) \[(?P<time_local>[^\]]+)\] "(?P<request>[^"]*)" (?P<status>\d+) (?P<body_bytes_sent>\d+)$'
(?P<name>...) 就是字段名,匹配到的内容会成为对应字段。写正则的诀窍:先拿 10-20 条真实样本,而不是对着日志格式文档猜。常用的命名模式(IP、TIMESTAMP_ISO8601、UUID)可以沉淀成 pattern 库复用,而不是每次都手写。还要当心正则性能:别用 .* 开头或嵌套量词这类容易触发灾难性回溯的写法,单条日志解析超过几毫秒就会拖慢整条采集管道。
多行合并。Java/Go 的异常堆栈、Python 的 traceback 是跨多行的,不合并的话一条异常会被拆成几十条日志,字段提取全乱。配置里按首行特征合并:
parsers:
- type: multiline
pattern: '^(\d{4}-\d{2}-\d{2}|\S+ \d+)'
即:以「日期开头」或「时间戳开头」的行作为新日志的起点,其余行拼到上一条。
提取出来的字段默认是字符串,但聚合需要正确的类型。把 status 转成整数、cost_ms 转成浮点、布尔转成布尔,这样告警里写 status >= 500 才对。字段命名也定个规范,建议统一小写加下划线(cost_ms 而不是 CostMs),和代码里的命名保持一致,检索时不用纠结大小写。
时间戳尤其重要:日志里的时间字段和采集时间可能差很多(跨时区、日志缓存延迟),显式指定时间字段和格式,避免「日志时间错乱」:
time:
field: time_local
format: "02/Jan/2006:15:04:05 -0700"
结构化之后顺手做两件事:一是脱敏,手机号、身份证、银行卡、token 在入库前就打码,敏感数据不进存储;二是字段裁剪,把 debug 日志里无用的长字段丢掉,索引体积和查询成本都降下来。
举例:一条订单日志里同时有手机号和 token,脱敏后手机号变成 138****1234,token 整段打码。脱敏要在采集端做,而不是查询时做——数据一旦明文入库,再想收回就难了。同理,一条带 4KB 请求体的 debug 日志,多半不值得占索引空间,直接把这个字段丢掉。这两条在生产上线前就配好,比事后补救省事得多。
解析规则配错的概率很高,别指望一次写对。OBSERVE 的解析器带「测试模式」:贴一段真实日志进去,实时看每一步解析后字段长什么样,配好了再上线。上线后也要抽查——按 source 分组看「解析失败率」,失败率高的日志单独拎出来看是什么格式没覆盖到。解析规则是活的,日志格式一改就得跟着改,建议把解析规则的变更和代码变更放在同一个评审流程里。如果发现某个日志格式长期解析不稳定,与其堆规则,不如回应用侧改格式——结构化日志的最佳来源,永远是应用直接输出 JSON。