Grok 的全部机制可以用一句话交代:%{PATTERN:field} 在管道启动时被递归展开成一段纯正则,字段名就是命名捕获组的组名,匹配交给一个回溯式正则引擎执行一次。它的性能上限、失败语义、调试手段都能从正则的性质直接推出来,不需要额外的心智模型。

这条线索往下追会落到三个具体问题:展开时哪些默认值决定了"到底有没有字段产出"、匹配失败与匹配超时为什么打的是两个不同的 tag、以及灾难性回溯除了"写 pattern 时小心"之外还有没有运行时的保险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
input event
message: "203.0.113.5 GET /api 200"


┌───────────────────────────────────────────┐
│ grok filter │
│ │
│ match => { "message" => "%{IP:client} │
│ %{WORD:verb} │
│ %{NOTSPACE:path} │
│ %{NUMBER:status:int}" } │
│ │
│ pattern alias resolution: │
│ %{IP} → (?<client>IP_regex) │
│ %{WORD} → (?<verb>\b\w+\b) │
│ ... │
│ │
│ Joni 引擎(Oniguruma 的 Java 移植) │
└─────────────┬─────────────────────────────┘

三种结局,标记各不相同:
匹配成功 → 写入命名捕获组对应的字段
所有 pattern 都不匹配 → tag: _grokparsefailure
超过 timeout_millis → tag: _groktimeout

Grok 不是新语言

Grok 经常被当成 Logstash 特有的"配置 DSL"。实际上它的全部机制只有两层:

第一层是命名捕获组。Grok 的 %{PATTERN:field} 语法在运行时展开成一个正则命名捕获组 (?<field>正则表达式)。字段 field 从捕获组的名字来,字段的值从捕获内容来。这是标准的 Oniguruma 正则语法,不是 Logstash 自定义的。

第二层是 pattern 别名库。Logstash 自带一套 pattern 文件(位于安装目录的 patterns/ 下),里面定义了 IPWORDNUMBERHTTPDATE 等几百个名字,每个名字对应一段正则字符串。%{IP} 在展开时就是把 IP 这个名字替换成它在 pattern 文件里对应的正则。pattern 可以互相引用,HTTPDATE 的定义里引用了 MONTHDAYMONTHYEARTIMEINT 等更基础的 pattern。

把这两层合起来看:Grok 的整个处理过程就是"递归展开 pattern 别名直到全部变成原生正则,然后对 message 字段做一次正则匹配,把命名捕获组的结果提取成 event 字段"。没有任何超出正则范畴的东西。

%{PATTERN:field:type} 的三段结构

Grok 语法里一个完整的 token 最多有三段:

1
2
3
4
5
6
7
%{PATTERN_NAME : field_name : type}
│ │ │
│ │ └── 可选:int 或 float
│ │ 匹配结果强制转换成对应类型
│ └── 可选:提取到的字段名
│ 不写则只做匹配,不存字段
└── 必填:pattern 库里的名字(或自定义 pattern 名)

type 字段目前支持 intfloat。不指定 type 时,所有提取结果都是字符串。%{NUMBER:bytes:int} 把匹配到的数字字符串直接转成整数,省去后续用 mutate 做类型转换的步骤。

只写 %{PATTERN} 不带 field_name 时,该 pattern 仍然参与匹配(可以用来跳过不关心的部分),但不会产生新字段。

"不带 field name 就不产生字段"是两个默认值共同造成的结果,不是语法层的必然。named_captures_only 默认 true,只把命名捕获写进 event,设成 false 之后匿名捕获也会落成字段。keep_empty_captures 默认 false,捕获到空字符串时那个字段直接不出现;pattern 里带可选段((?:...)?)时,这个默认值就是"字段有时在有时不在"的来源,改成 true 能让字段稳定存在、值为空串。

自定义 pattern

当内置 pattern 不满足需求时,有两种方式添加自定义 pattern:

方式一:在 patterns_dir 指向的目录里放文件,文件里每行一个 pattern 定义:

1
2
3
# 文件:/etc/logstash/patterns/custom
ORDERID [A-Z]{2}-\d{8}
TXSTATUS (SUCCESS|FAILED|PENDING)

方式二:在 grok 配置里用 pattern_definitions 内联定义:

1
2
3
4
5
6
7
8
9
filter {
grok {
pattern_definitions => {
"ORDERID" => "[A-Z]{2}-\\d{8}"
"TXSTATUS" => "(SUCCESS|FAILED|PENDING)"
}
match => { "message" => "%{ORDERID:order_id} %{TXSTATUS:status}" }
}
}

内联定义适合只用一两次的局部 pattern;如果多个管道或多个 grok block 复用同一批 pattern,放文件更清晰。

两套内置 pattern:legacy 与 ecs-v1

内置 pattern 库不是一份而是两份。logstash-patterns-corepatterns/ 下有 legacy/ecs-v1/ 两个平行目录,同名 pattern 在两边都存在,正则骨架一样,捕获组名不一样。加载哪一份由 grok filter 的 ecs_compatibility 决定:disabled 用 legacy,v1v8 用 ECS 版。

对基础 pattern(IPWORDNUMBER)这个区别看不出来,它们本身不带字段名。区别集中在复合 pattern 上,因为它们的定义里写死了字段名,换目录等于换掉整套输出字段。COMBINEDAPACHELOG 展开后的字段名对照如下:

ecs_compatibility => disabled ecs_compatibility => v1 / v8
客户端地址 clientip [source][address]
请求方法 verb [http][request][method]
请求路径 request [url][original]
状态码 response(字符串) [http][response][status_code](定义里已内联 :int
响应字节数 bytes(字符串) [http][response][body][bytes](同样内联 :int
User-Agent agent [user_agent][original]

同一个 pattern 名、同一行输入,两种模式下 Kibana 里能查到的字段名完全不同,建在上面的 dashboard 与告警规则也就不通用。这个开关的默认值取决于 pipeline.ecs_compatibility,随 Logstash 版本而变,所以生产配置里应当显式写出 ecs_compatibility 的取值,不要依赖默认。

pattern 目录内部还有一层切分:基础 pattern 集中在 grok-patterns 文件里,按软件分组的复合 pattern 放在同级的 httpdjavalinux-syslog 等文件里。COMBINEDAPACHELOGhttpdgrok-patterns 里搜不到它。这个切分本身就是"别名可递归组合"的实物证据:从 COMBINEDAPACHELOG 展开到 IPORHOSTHTTPDATE 这类基础 pattern,中间要过 HTTPD_COMBINEDLOGHTTPD_COMMONLOG 两跳,而 HTTPDATE 自己还要再往下展开一层。

多 pattern 顺序匹配

grok 的 match 接受一个字段对多个 pattern 的列表:

1
2
3
4
5
6
7
8
9
10
11
filter {
grok {
match => {
"message" => [
"%{COMBINEDAPACHELOG}",
"%{SYSLOGLINE}",
"%{GREEDYDATA:raw}"
]
}
}
}

Logstash 按列表顺序逐一尝试,第一个匹配成功就停止。"停止"这个行为由 break_on_match 控制,默认 true%{GREEDYDATA:raw} 通常作为"兜底 pattern"放在列表末尾,确保任何一行都能被捕获(哪怕只是作为整体放进 raw 字段),避免大面积产生 _grokparsefailure

默认值下开销这样累积:每个 pattern 都要跑一次完整匹配,命中时前面失败的尝试已经付掉了,全不命中时整个列表都要付一遍。所以列表越长,失败时越贵。把最常见的日志格式放在靠前位置能减少平均尝试次数。

break_on_match 设成 false,行为变成"所有 pattern 全跑一遍,各自的捕获结果合并进同一个 event"。适用场景是一行日志里混着几段互不重叠的结构,需要分头提取。此时上面那条排序优化失效:不管顺序如何,每条 event 都要付满全部 pattern 的开销。

match 的值也不止是"一个字段配一个 pattern 列表"这一种形态。它可以是多个字段各配自己的 pattern(match => { "field1" => "...", "field2" => "..." }),各字段独立匹配;一个配置文件里更可以串联多个 grok 块,每块处理不同字段。

_grokparsefailure:触发条件与处理策略

当所有 pattern 都没有匹配成功时,grok filter 向 event 的 tags 数组添加字符串 _grokparsefailure,event 的其他字段保持原样(包括 message)。event 继续向下流,不会被丢弃——丢弃是 output 或显式 drop {} 的职责,不是 grok 的默认行为。

这个 tag 只覆盖"跑完了但没匹配上"这一种失败。还有一种是"没跑完就被掐了",即匹配超时,打的 tag 是 _groktimeout,机制见下面的性能一节。两者的语义不同,按 tag 路由时要分开处理。

几种常见处理策略:

策略一:在 output 里按 tag 路由,把 _grokparsefailure 的 event 发到单独的索引或文件,便于后续人工检查:

1
2
3
4
5
6
7
output {
if "_grokparsefailure" in [tags] {
file { path => "/var/log/logstash/grok-failures-%{+YYYY-MM-dd}.log" }
} else {
elasticsearch { ... }
}
}

策略二:用 tag_on_failure 覆盖默认 tag 名,在多个 grok block 并存时便于区分是哪个 block 失败:

1
2
3
4
5
6
filter {
grok {
match => { ... }
tag_on_failure => ["_grok_access_failure"]
}
}

策略三:用 Grok 调试器(Kibana Dev Tools 的 Grok Debugger,或 https://grokdebugger.com)先离线调试 pattern,确认匹配后再放进生产配置。它省掉的是每改一次 pattern 就要重启 Logstash、重新预热 JVM 与 JRuby 的那份固定成本,而这份成本与 pattern 本身的复杂度无关。

最小实验:观察 Grok 展开

用 stdin/stdout 管道验证 Grok 的行为:

1
2
3
4
5
6
7
8
9
10
# grok-demo.conf
input { stdin {} }
filter {
grok {
match => {
"message" => "%{IP:client_ip} %{WORD:http_verb} %{NOTSPACE:request_path} %{NUMBER:status_code:int}"
}
}
}
output { stdout { codec => rubydebug } }
1
bin/logstash -f grok-demo.conf

输入一行测试数据:

1
203.0.113.5 GET /api/v1/orders 200

期望输出(关键字段):

1
2
3
4
5
6
7
8
{
"client_ip" => "203.0.113.5",
"http_verb" => "GET",
"request_path" => "/api/v1/orders",
"status_code" => 200, ← 整数,不是字符串
"message" => "203.0.113.5 GET /api/v1/orders 200",
...
}

再输入一行无法匹配的文本(比如 hello world),观察输出里 tags 字段包含 _grokparsefailure,且 message 字段保留原始内容。

把上述实验对应到内部对象:grok filter 在初始化阶段把 %{IP:client_ip} 展开成完整正则(包含 IPv4 和 IPv6 的命名捕获组),编译成正则对象并缓存。每条 event 到达时,直接用已编译的对象对 message 字段执行一次匹配,把命名捕获组的结果写入 event 字段。

性能:Joni 引擎与灾难性回溯

Grok 的正则语法族是 Oniguruma。但 Logstash 跑在 JRuby 上,实际执行匹配的引擎是 Joni,也就是 Oniguruma 的 Java 移植(MRI Ruby 用的是另一个 fork Onigmo)。三者同属回溯式 NFA,所以回溯风险与具体落在哪个实现上无关。

回溯是正则处理非确定性匹配的标准机制,但在某些 pattern 组合下回溯深度会指数增长,把单条 event 的处理时间从微秒推到秒级。常见触发场景是嵌套量词,例如 (a+)+b 遇到无法匹配的输入。内置的 GREEDYDATA(展开成 .*)与其他贪婪量词嵌套时同样危险。

回溯风险有一道运行时保险,它不依赖人事先猜到哪个 pattern 危险,因此比任何写法层的技巧都可靠。grok filter 有 timeout_millis,默认 30000:超过这个时长就中止匹配,给 event 打上 tag_on_timeout 指定的 tag(默认 _groktimeout)。判定按 250ms 量化,只会晚不会早;设成 0 才是彻底关掉超时。

配套的 timeout_scope 默认 pattern,含义是超时按每个 pattern 单独计。多 pattern 列表下,这个默认值让每一次尝试都各自付一份超时管理开销,开销随列表长度叠加。官方建议改成 event,让整条 event 只有一个总预算,保护力度相同而开销明显更低。它和 break_on_match 的默认值是叠加的:列表越长,失败时不仅要付满全部匹配开销,还要付满全部超时管理开销。

_groktimeout_grokparsefailure 应当分开路由。前者说明这条 pattern 在这类输入上有回溯风险,是"重写 pattern"的信号;后者只说明 pattern 与日志格式对不上,是"更新 pattern"的信号。混进同一个错误索引里,回溯问题会被日常格式漂移的噪声埋掉。

写法层的规避手段都在做同一件事:缩小正则引擎的搜索空间。用 ^$(或 \A/\Z)把 pattern 锚定到行首行尾,防止引擎在无法匹配时从每个字符位置重新起跳;用明确的字符集替代 .*,例如 %{NOTSPACE} 展开成 \S+,每一步都能靠边界字符快速判定成败,在有分隔符的输入上比 .*? 快得多。Kibana Grok Debugger 会显示匹配的捕获与尝试次数,数字异常高的 pattern 就是重写候选。

这些手段降低回溯的概率,但不消除回溯。日志有固定分隔符时还有一条更彻底的路:换掉正则引擎本身,用线性扫描的 dissect 做提取,把 Grok 留给真正需要正则的非结构化部分。这是下一篇的主题。

模式提炼

1
2
3
4
5
6
7
8
9
10
模式:别名展开 + 命名捕获 = 无新语法的结构提取

- 给复杂正则起一个语义名字(pattern alias),让使用者写 %{HTTP_METHOD}
而不是 (?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)
- 命名捕获组把匹配结果和字段名绑定,消除"第 3 个捕获组是什么意思"的问题
- 别名可以递归组合,从基础 pattern(NUMBER、WORD)构建高层 pattern(COMBINEDAPACHELOG)
- 失败路径显式标记,event 不丢,下游可按 tag 路由处理异常数据
_grokparsefailure 跑完了但没匹配上 → pattern 与格式对不上
_groktimeout 没跑完就被掐了 → pattern 有回溯风险
- 字段名不由语法决定,由 pattern 库的哪一套定义决定(ecs_compatibility)

这个模式在其他工具里也有对应:Fluentd 的 parser 插件支持命名捕获正则;Python 的 regex 库支持 (?P<name>...);ClickHouse 的 extractAll 函数支持命名组。把正则和字段名绑定的思路是通用的。

工程迁移表

Logstash Grok 概念 Fluentd 对应 通用正则处理对应 结构化日志替代方案
%{PATTERN:field} (?<field>regex) in parser Python re 命名组 JSON 直接解析,无需正则
pattern 别名库 Fluentd 内置 parser(apache2 等) 预编译正则常量 schema 字段定义
_grokparsefailure tag emit_invalid_record_to_error 异常捕获 + 错误队列 解析报错 + 死信路由
tag_on_failure error_label 自定义异常标记 ETL 错误分类
Joni 回溯(Oniguruma 语法族) 同族(Onigmo,CRuby 引擎) PCRE 回溯 无回溯(dissect / RE2)
patterns_dir 自定义 自定义 parser 类 正则常量模块 schema 注册表

常见误解

误解一:“Grok pattern 匹配失败就意味着日志格式错了”。匹配失败通常意味着 pattern 写得不够准确,而不是日志本身有问题。_grokparsefailure 最常见的原因是日志格式有细微变体(多一个空格、时间格式不同、新版本服务改了日志布局),pattern 没有跟着更新。调试时先看日志样本,再看 pattern,不要先怀疑日志。

误解二:“用 %{GREEDYDATA} 兜底就不会有 _grokparsefailure”。%{GREEDYDATA} 对任意字符串都能匹配(包括空字符串),确实能消除 _grokparsefailure,但代价是所有没被前面 pattern 匹配到的内容都会被塞进一个字段,失去结构化提取的意义。兜底 pattern 适合用在"能结构化就结构化,不能就先存原文"的分级处理策略里,而不是掩盖 pattern 写错的问题。

误解三:“Grok 比 JSON codec 慢,所以要尽量避免用 Grok”。这个对比没有意义。JSON codec 解析的是结构化的 JSON 输入;Grok 处理的是非结构化文本。如果日志本身就是 JSON,当然直接用 json codec,完全不需要 Grok。如果日志是 Apache access log 这类文本格式,没有 Grok 就必须手写正则。两者适用场景不重叠。

误解四:“把 timeout_millis 调大或关掉,最坏也就是慢一点”。grok 的匹配是在 pipeline worker 线程上同步跑完的,一条触发灾难性回溯的输入会把这个 worker 独占到匹配结束。pipeline.workers 是 8 的机器上,一条这样的日志就吃掉八分之一的处理能力;同类日志成批到达时,整条管道会被拖到近乎停滞,队列随之填满、背压一路传回 input。默认那 30 秒不是"慢一点"的上限,而是单条 event 能占住一个 worker 的时长上限。

练习

  1. 取一行真实的 Nginx access log,先在配置里显式固定 ecs_compatibility(分别试 disabledv8 各跑一次),用 %{COMBINEDAPACHELOG} 匹配,对比两次拿到的字段名。再去 Logstash 安装目录里找它的定义:vendor/bundle/jruby/*/gems/logstash-patterns-core-*/patterns/{legacy,ecs-v1}/httpd。在 grok-patterns 文件里 grep 这个名字会一无所获,按上一节说的目录切分想一下为什么。顺着定义往下追,把它展开到基础 pattern 为止。

  2. 写一个 grok pattern 匹配形如 ORDER-AB-12345678 SUCCESS 99.50 的自定义日志格式,提取 order_id(字符串)、status(字符串)、amount(float)。用本文的 stdin/stdout 实验配置验证。

  3. 思考题:写一个会触发 _grokparsefailure 的输入(格式不匹配),观察 event 的 tags 字段和 message 字段。在 filter 里加一个条件块,对这个 tag 和 _groktimeout 分别加不同的字段值,让两类失败在下游可区分。解释为什么 Grok 不直接丢弃不匹配的 event 而是加 tag 继续流转,以及为什么这两个 tag 不该合并成一个。

系列导航

序号 主题
00 导读:核心对象是 event,骨架是三段管道
01 架构:JRuby、JVM 与 pipeline 的运行形态
02 event 模型:@timestamp、@metadata 与字段引用
03 codec:字节流与 event 的边界转换
04 input 插件:拉取、监听与 Beats 接入
05 Grok 的本质:命名正则加预定义 pattern(本篇)
06 dissect 与结构化 filter:放弃回溯换吞吐
07 常用 filter 组合:mutate、date、geoip 与条件
08 output 插件:Elasticsearch output 与批量写入
09 pipeline 执行模型:worker、batch 与背压
10 内存队列 vs 持久队列:可靠性的分界线
11 死信队列(DLQ):无法处理的 event 去哪
12 Multiple Pipelines 与 pipeline-to-pipeline
13 监控:Node Stats API、hot threads 与瓶颈定位
14 性能调优:JVM heap、批处理与持久队列磁盘
15 Logstash vs Beats vs Ingest Pipeline:该用谁
16 Logstash vs Fluentd vs Vector:日志管道的三种取舍
17 Logstash 的演进与 Elastic Agent 的冲击

参考资料