本节摘要:非结构化日志在采集后无法被机器有效检索和分析。本节先给出结构化日志的 JSON 模板,用"谁能五秒从海量日志里定位问题"这个标准说服你为什么要改。然后补上真实世界不可能全 JSON 的现实:旧系统的自由文本怎么通过 Grok 解析成字段。附一个把 Nginx 访问日志和一条错误日志分别结构化的完整例子。
非结构化日志是灾难。凌晨你被叫醒,看到一条 2026-09-01 03:15:00 ERROR payment failed for user=3842 amount=299 reason=timeout,如果你只是搜索"payment failed",可能会在海量日志里乱翻十分钟。而如果你把它写成 JSON:
{ "timestamp": "2026-09-01T03:15:00+08:00", "level": "ERROR", "service": "payment-gateway", "message": "payment failed", "user_id": "3842", "amount": 299, "reason": "timeout", "trace_id": "a1b2c3d4" }
那么你可以直接做 service:payment-gateway AND reason:timeout 的精确筛选,在五秒内定位到所有相同根因的日志。这就是结构化与自由文本的区别:前者给机器一个能用的搜索维度,后者只能给人类一双好眼睛——但凌晨三点你的视力是靠不住的。
最紧凑的结构化日志必须至少包含以下几个字段:
在这之上,再按业务场景补充关键维度,如 user_id、order_id、request_path、response_time_ms。字段选择遵循一条原则:你凌晨三点最可能拿来搜的那几个维度,必须全部上。比如用户投诉说"我这笔订单失败了",你最可能搜 order_id,所以 order_id 必须在结构化字段里。
不可能所有服务都改成 JSON,旧系统、第三方系统、操作系统日志往往是一行文本。这种时候靠 Grok——它是 Logstash 里的模式匹配工具,能把自由文本按正则拆成字段。
用一个最现实的例子:Nginx 的 access log 默认格式是这样的:
192.168.1.100 - - [01/Sep/2026:03:15:00 +0800] "GET /api/pay?order=123 HTTP/1.1" 200 42 "-" "Mozilla/5.0"
给它一条 Grok 模式:
%{IP:client_ip} - - \[%{HTTPDATE:timestamp}\] "%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}" %{NUMBER:status} %{NUMBER:body_bytes} "%{DATA:referer}" "%{DATA:user_agent}"
解析后你就得到了 client_ip、timestamp、method、request、status 等字段,可以精确搜索 status:500 或者按 client_ip 聚合访问量。Grok 不是银弹,模式越复杂匹配越慢,但它的存在让旧系统也能纳入结构化查询体系。
我的建议:新项目直接从源头打 JSON,旧项目先用 Grok 过渡,逐步推动改造。先动起来,不要等待一次完美的大重构——那通常意味着永远做不成。
实战中日志解析不是直接入口入库,而是有一条轻量管道:采集端先把原始日志送到一个消息队列,然后由解析服务(Logstash 或自定义的)做解析,解析完再进存储。这样做的好处是:原始日志先攒起来,如果解析规则改错,可以重跑;如果解析挂了,日志不丢。
日志进生产后不会一直顺,早晚会遇到解析失败。常见几种:字段类型对不上(本该 int 变成文本)、模式匹配不上(新版本改了日志格式)、字段缺失(漏打了一个维度)。我的兜底三层法:第一,解析不搞"一次必成",先在队列里攒盘,再分批解析,解析失败回写死信队列(如上图),不吞日志;第二,失败要能看见,给死信队列加监控计量,接进第二章的告警,避免"解析挂了十天没人知";第三,版本化解析规则,每次改解析模板都留版本,万一那批日志格式变了,能回滚到能解析的旧版本重跑。光靠"写完一次就永远对",在真实系统里基本不成立。
结构化打好了,下一节处理"怎么把这些日志送到集中仓"——从采集到缓冲,从 Agent 到消息队列,每一步都有坑。这是数据在流动时的安保工作。