深入 Logstash 02 - event 模型:@timestamp、@metadata 与字段引用
在 filter 里写 add_field => { "[@metadata][target_index]" => "logs-web" },output 的 index => 里用 %{[@metadata][target_index]} 能取到值,但写进 Elasticsearch 的 _source 里找不到这个字段。这件事的机制既不在 output 插件里,也不在 codec 里,而在 Event API 上:org.logstash.Event 有两个平级的 ConvertedMap 字段 data 和 metadata,to_hash 返回的只是 data,要连 metadata 一起拿必须改调 to_hash_with_metadata。凡是走 to_hash 的下游,看到的就是一个没有 @metadata 的 event——不需要任何人动手剥离。
上一篇确立了 JRuby/JVM/多 pipeline 的运行形态和本系列的版本前提。这一篇进入 event 的内部结构:字段空间怎么分层,@timestamp 的语义边界在哪,以及字段引用语法里那几对看着等价其实不等价的写法。
数据流全景
1 | |
@timestamp 和 @version 在 data 里,和 message、status 这些业务字段住在同一个 map,区别只在于命名约定和插件对它们的特殊对待。@metadata 则不在 data 里:Event 的构造函数会把传入 map 里的 @metadata 键取出来,搬进那个独立的 metadata 字段。所以"@metadata 不进输出"不是一次剥离动作的结果,而是它从来就没在被序列化的那份数据里。
@timestamp:第一等公民
@timestamp 是 event 进入 pipeline 的时间标记,类型为 LogStash::Timestamp,底层对应 java.time.Instant。
自 Logstash 8.0 起,内部的时间表示支持纳秒粒度,序列化成 ISO-8601 字符串时最多带 9 位小数:
1 | |
实际能拿到多少位取决于 JVM 与平台的时钟分辨率,官方对此的措辞是多数情况下到微秒;纳秒位为 0 时 Timestamp.toString() 退回固定 3 位小数。这件事和 pipeline.ecs_compatibility 是两回事——后者管的是插件写字段时的命名与落位(host 还是 [host][hostname]),跟时间精度无关。
行为要点:
- input 插件在 decode 阶段(codec 调用
LogStash::Event.new)自动赋值为当前时间;如果原始数据中已有时间字段,需要显式用datefilter 解析并覆盖@timestamp,否则它记录的是到达时间而非事件发生时间。 @timestamp是可以删掉的。Event.remove对它没有任何保护,走的是和普通字段一样的删除分支,event.remove("[@timestamp]")会成功。删掉之后getTimestamp()返回 null,%{+YYYY.MM.dd}一类的时间插值会展开成空串,Elasticsearch 侧的时间路由和 data stream 写入也都会出问题。所以实践上只用datefilter 覆盖它,不删。- Elasticsearch output 默认将
@timestamp映射为_source.@timestamp,同时用于索引名称模板(如logstash-%{+YYYY.MM.dd})。
@metadata:管道内部暂存区
@metadata 在 filter 阶段可以像普通字段一样读写,但它不在 Event 的 data 里,所以任何调 to_hash 取数据的下游都看不到它。
Elasticsearch output 是这条机制最容易被讲错的地方。它根本没有 codec 配置项——bulk 请求体是它自己拼的,每个 event 走一次 event.to_hash 转成 JSON 放进 action 三元组。这条路径上没有任何"剥离 metadata"的代码,@metadata 不进 _source 完全是因为它不在 to_hash 的返回值里。
反过来,会显示 metadata 的只有主动去要的那一方。rubydebug codec 的 metadata 选项默认 false,此时走 event.to_hash;设成 true 之后它改调 event.to_hash_with_metadata,才会把 @metadata 打出来。
典型用途:
- 携带路由决策信息(
[@metadata][target_index]),在 output 中引用但不写入 Elasticsearch。 - 记录重试次数或处理标记,避免污染业务字段。
- Beats input 把协议侧信息放进这里:
[@metadata][beat]、[@metadata][version],ECS 模式下还有[@metadata][input][beats][host][name]与[@metadata][input][beats][host][ip]。filter 阶段可读取,但不会意外输出到下游。
1 | |
字段引用语法
Logstash 使用方括号语法引用字段,与 JSON path 类似但有自己的规则:
| 写法 | 含义 |
|---|---|
[message] |
顶层字段 message |
[host][hostname] |
嵌套字段:host 下的 hostname。注意不能写成 host.hostname,那会被解析成一个名叫 host.hostname 的顶层字段 |
[@timestamp] |
保留字段(@前缀) |
[@metadata][key] |
@metadata 子字段 |
%{[field]} |
sprintf 格式,用于字符串插值 |
%{+YYYY.MM.dd} |
Joda-Time 格式化 @timestamp;官方已把 joda 格式标为 deprecated |
%{{yyyy.MM.dd}} |
java.time 格式化 @timestamp,官方推荐的写法,注意是两层花括号 |
%{{TIME_NOW}} |
取求值那一刻的当前时间,而不是 @timestamp,用来量各阶段耗时 |
字段引用在 if 条件、mutate 插件、output 的 index/document_id 参数中普遍使用。
新旧两种日期格式不能互换:%{+YYYY.MM.dd} 里的模式串交给 Joda 的 DateTimeFormat,%{{yyyy.MM.dd}} 交给 java.time.format.DateTimeFormatter,同一个字母在两套体系里的语义未必一致。已有配置里的 joda 写法仍然工作,新写的建议直接用双花括号那种。
1 | |
filter 插件的 event API
filter 插件通过 event 对象操作字段:
| 方法 | 说明 |
|---|---|
event.get("[field]") |
读取字段值,不存在时返回 nil |
event.set("[field]", value) |
写入字段值 |
event.remove("[field]") |
删除字段 |
event.tag("_error") |
向 tags 数组追加标签 |
event.cancel |
取消 event,阻止其进入 output |
event.to_hash |
取 data,不含 @metadata |
event.to_hash_with_metadata |
取 data 并在 @metadata 非空时把它挂上 |
在 ruby filter 中可直接调用这些方法:
1 | |
类型与强制转换
所有从文本来源(stdin、file input 的单行读取)进入 pipeline 的字段,初始类型均为字符串。数值比较或数值运算前需要显式转换:
1 | |
Beats input 走 lumberjack v2 协议,数据帧的 payload 是 JSON(可选 zlib 压缩,帧类型上区分 CODE_JSON_FRAME 与 CODE_COMPRESSED_FRAME)。JSON 本身带类型,数字和布尔值解码后就是数字和布尔值,所以从 Beats 来的字段不需要再走一遍 convert。
可运行实验
目标:在同一次运行里让同一个 event 走两条不同的取数路径,直接看出 @metadata 的可见性由谁决定。
1 | |
1 | |
两块 stdout 的打印顺序不保证,但内容差异是确定的。默认那块:
1 | |
带 metadata => true 的那块多出一整个 @metadata:
1 | |
/tmp/logstash_out.json 里的那一行:
1 | |
同一个 event、同一次运行,三处输出的差别只在于下游取的是 data 还是 data 加 metadata:to_hash 与 to_json 都只碰 data,只有 to_hash_with_metadata 会把 metadata 挂上去。
把命令行里的 --pipeline.ecs_compatibility=disabled 去掉重跑一遍,host 会变成 "host" => {"hostname" => "web-01"},并且多出一个 [event][original]。这是 ECS v8 默认带来的字段落位差异,和 @metadata 的可见性是两条互不相干的机制。
再演示 sprintf 字段引用:
1 | |
关键对象映射
| event 概念 | 对应 Java/JRuby 类 | 说明 |
|---|---|---|
| event 本体 | org.logstash.Event |
持有两个平级字段 ConvertedMap data 与 ConvertedMap metadata |
| @timestamp | org.logstash.Timestamp |
封装 java.time.Instant,存放在 data 里 |
| @metadata | Event 的 metadata 字段 |
构造时从传入 map 里摘出来,不在 data 里 |
| 字段引用解析 | FieldReference |
将 [a][b] 解析为路径数组,@metadata 前缀走 META_PARENT/META_CHILD 分支 |
| tag 操作 | tags 字段(字符串数组) |
event.tag(x) 追加 |
模式提炼
event 的字段空间在实现上只分两块:data 和 metadata。@timestamp、@version 和业务字段一起住在 data 里,靠命名约定和插件的特殊对待区分;metadata 是完全独立的另一个 map。
由此推出的是一条比"处理完成后剥离"更硬的判据:一个字段会不会进输出,只取决于它在哪个 map 里,以及下游插件调的是 to_hash 还是 to_hash_with_metadata。不需要去查每个 output 插件或每个 codec 有没有做剥离动作——那层代码根本不存在。同一个模式在别处也成立:数据结构上的隔离比流程上的过滤更可靠,因为前者没有"忘了过滤"这种失效方式。
@timestamp 的语义是事件的时间维度锚点,date filter 的职责是把原始日志里的时间字符串解析后写进 @timestamp,使它从到达时间变为发生时间。这一步是精确时序分析的前提。
工程迁移表
| Logstash event 概念 | Kafka Record 对应 | Flink StreamRecord 对应 | 结构化日志对应 |
|---|---|---|---|
| 业务字段 | value(反序列化后的 Map) | value | log fields (level/msg/…) |
| @timestamp | timestamp(毫秒 epoch) | timestamp + watermark | timestamp 字段 |
| @metadata | headers(producer/consumer 元数据) | 无直接对应,常用 SideOutput | MDC / context map(不输出) |
| @version | 无直接对应 | 无直接对应 | schema version 字段 |
| event.cancel | filter 中 drop()或不 forward | filter 中不调用 collector.collect | 过滤条件 |
| tags 数组 | 无直接对应,常用 header | 无直接对应 | labels / tags map |
常见误解
误解一:“@metadata 是 Logstash 自动加的,不用管。”
实际情况:这话把责任方搞反了。Logstash 核心几乎不往里面写东西,@metadata 的内容基本来自 input 插件和使用者自己写的 filter,语义完全由配置决定。当成"系统自动维护的角落"看待,就会错过用它承载路由决策和处理状态的整个用法——那是这块空间存在的理由。
误解二:“@timestamp 就是 Logstash 收到日志的时间,直接用就行。”
实际情况:不经过 date filter 处理时,@timestamp 是 event 进入 input codec 的时刻,而非日志的原始发生时间。对于有历史回溯需求或时序分析的场景,必须用 date filter 从 message 中解析真实时间并覆盖 @timestamp。
误解三:“字段引用 [host] 和 [host][hostname] 是一样的。”
实际情况:前者引用整个 host 对象(ECS 模式下是一个 map),后者引用它内部的 hostname 子字段(字符串)。在条件表达式中混用会导致类型不匹配。而如果为了省事写成 host.hostname,语法上合法但语义完全不同:那是一个名字里带点号的顶层字段,取不到任何东西。
误解四:“rubydebug 输出就是最终写入 Elasticsearch 的内容。”
实际情况:两处对不上。一是 @metadata:rubydebug 的 metadata 默认 false,开成 true 之后会多打一整个 @metadata,而 Elasticsearch 拿到的永远没有它。二是格式:rubydebug 打的是 Ruby 的 inspect 形式,@timestamp 显示为 Timestamp 对象,而 _source 里是 ISO-8601 字符串。用 rubydebug 确认字段结构没问题,把它当 _source 的预览就会踩坑。
练习
-
编写一条 pipeline,从 stdin 读取 Apache access log 格式的文本(如
127.0.0.1 - - [06/Aug/2026:13:20:00 +0800] "GET / HTTP/1.1" 200 1234),用grokfilter 提取字段,再用datefilter 将提取的时间覆盖@timestamp,最后用 rubydebug 输出,对比处理前后@timestamp的值。然后在 output 里同时用%{+YYYY.MM.dd}和%{{yyyy.MM.dd}}生成两个字段,确认它们对同一个@timestamp给出相同结果。 -
用
rubyfilter 在同一个 event 上先后调用event.to_hash.keys和event.to_hash_with_metadata.keys,把两个结果分别写进两个普通字段再输出,看@metadata出现在哪一个里面。然后试着event.remove("[@timestamp]"),观察删除是否成功、%{+YYYY.MM.dd}展开成了什么。
系列导航
参考资料
- Logstash Event API 文档:https://www.elastic.co/guide/en/logstash/current/event-api.html(
get/set/remove/to_hash的契约) - 字段引用语法详解:https://www.elastic.co/guide/en/logstash/current/field-references-deepdive.html(
[a][b]的形式语法,以及为什么点号不是路径分隔符) - 配置里访问 event 数据:https://www.elastic.co/guide/en/logstash/current/event-dependent-configuration.html(sprintf 的三套日期语法,joda 已标 deprecated 的原文与
%{{TIME_NOW}}的用法) @metadata字段小节:https://www.elastic.co/guide/en/logstash/current/event-dependent-configuration.html#metadata(用途约定,以及为什么它不进输出)- date filter 文档:https://www.elastic.co/guide/en/logstash/current/plugins-filters-date.html(
target默认@timestamp、timezone缺省行为) - Event 的 Java 实现:https://github.com/elastic/logstash/blob/main/logstash-core/src/main/java/org/logstash/Event.java(
data与metadata两个字段的声明、构造函数里摘@metadata的那几行、remove的分支) - JRuby 侧的 Event 绑定:https://github.com/elastic/logstash/blob/main/logstash-core/src/main/java/org/logstash/ext/JrubyEventExtLibrary.java(
to_hash与to_hash_with_metadata的实现差异) - rubydebug codec 源码:https://github.com/logstash-plugins/logstash-codec-rubydebug/blob/main/lib/logstash/codecs/rubydebug.rb(
metadata默认值,以及它据此切换调用哪个方法) - Beats input 源码:https://github.com/logstash-plugins/logstash-input-beats(lumberjack v2 的帧类型,与它写入
@metadata的字段清单)
