上一篇确立了 JRuby/JVM/多 pipeline 的运行形态。这一篇进入 event 的内部结构。event 容易被误解成"就是一个 JSON 对象"。更准确的说法是:event 是 Logstash 在管道内部流通的核心数据单元,由 Java 对象实现,包含业务字段、系统保留字段(@timestamp@version)和管道内部暂存区(@metadata),三者在语义上截然不同。本文只抓一个问题:event 的字段空间如何分层,以及 @metadata 为什么不进最终输出。

数据流全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Input (stdin/beats/kafka/...)

│ bytes

[ Codec: decode ]

│ 构造 Event 对象

┌───────────────────────────────────────────────┐
│ org.logstash.Event │
│ │
│ 业务字段空间 │
│ ┌─────────────────────────────────────────┐ │
│ │ message: "GET /index.html 200" │ │
│ │ host: { hostname: "web-01" } │ │
│ │ status: 200 │ │
│ │ ... │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 系统保留字段 │
│ ┌──────────────────────┐ │
│ │ @timestamp: 2026-... │ (不可删除) │
│ │ @version: "1" │ (几乎不变) │
│ └──────────────────────┘ │
│ │
│ 管道内部暂存区(序列化时剥离) │
│ ┌──────────────────────────────────────┐ │
│ │ @metadata: { pipeline: "main", │ │
│ │ retry_count: 2 } │ │
│ └──────────────────────────────────────┘ │
└───────────────────────────────────────────────┘

│ Filter 阶段:读/写字段,@metadata 参与

[ Codec: encode ] → @metadata 被剥离,不写入输出


Output (elasticsearch/kafka/file/...)

@timestamp:第一等公民

@timestamp 是 event 进入 pipeline 的时间标记,类型为 LogStash::Timestamp,底层对应 java.time.Instant

自 Logstash 8.0 起,@timestamp 支持纳秒精度(由 pipeline.ecs_compatibility 配置控制,默认 v8)。在 ECS v8 兼容模式下,@timestamp 序列化为 ISO-8601 字符串,精确到纳秒,例如:

1
2026-08-06T13:20:00.123456789Z

行为要点:

  • input 插件在 decode 阶段(codec 调用 LogStash::Event.new)自动赋值为当前时间;如果原始数据中已有时间字段,需要显式用 date filter 解析并覆盖 @timestamp,否则它记录的是到达时间而非事件发生时间。
  • @timestamp 不可删除。调用 event.remove("[@timestamp]") 在当前 8.x 版本中会抛出异常或被静默忽略(具体行为视版本而定),应通过 date filter 修改而非删除。
  • Elasticsearch output 默认将 @timestamp 映射为 _source.@timestamp,同时用于索引名称模板(如 logstash-%{+YYYY.MM.dd})。

@metadata:管道内部暂存区

@metadata 是一个 map 型字段,在 filter 阶段可以自由读写,但在 output 的 codec encode 阶段被剥离,不出现在最终序列化结果中。

典型用途:

  • 携带路由决策信息([@metadata][target_index]),在 output 中引用但不写入 Elasticsearch。
  • 记录重试次数或处理标记,避免污染业务字段。
  • Beats 输入将部分协议元数据(如 [@metadata][beat][@metadata][version])放入此空间,filter 阶段可读取但不会意外输出到下游。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# logstash.conf 示例
filter {
mutate {
add_field => {
"[@metadata][target_index]" => "logs-%{[service][name]}"
"[@metadata][retry_count]" => "0"
}
}
}

output {
elasticsearch {
index => "%{[@metadata][target_index]}"
# target_index 不会出现在 _source 中
}
}

字段引用语法

Logstash 使用方括号语法引用字段,与 JSON path 类似但有自己的规则:

写法 含义
[message] 顶层字段 message
[host][hostname] 嵌套字段,等价于 host.hostname
[@timestamp] 保留字段(@前缀)
[@metadata][key] @metadata 子字段
%{[field]} sprintf 格式,用于字符串插值
%{+YYYY.MM.dd} Joda-Time 格式,仅用于 @timestamp 的日期格式化

字段引用在 if 条件、mutate 插件、output 的 index/document_id 参数中普遍使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 条件分支
filter {
if [status] >= 400 {
mutate { add_tag => ["error"] }
}
}

# sprintf 插值
output {
file {
path => "/var/log/logstash/%{[host][hostname]}-%{+YYYY-MM-dd}.log"
}
}

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 获取所有业务字段(不含 @metadata)

ruby filter 中可直接调用这些方法:

1
2
3
4
5
6
7
8
9
filter {
ruby {
code => '
count = event.get("[@metadata][retry_count]").to_i
event.set("[@metadata][retry_count]", count + 1)
event.cancel if count > 3
'
}
}

类型与强制转换

所有从文本来源(stdin、file input 的单行读取)进入 pipeline 的字段,初始类型均为字符串。数值比较或数值运算前需要显式转换:

1
2
3
4
5
6
7
8
9
filter {
mutate {
convert => {
"status" => "integer"
"response_time" => "float"
"retried" => "boolean"
}
}
}

Beats input 在传输层使用 JSON 或 Protobuf 编码,字段类型在到达 Logstash 时已保留,无需额外转换。

可运行实验

目标:观察 @metadata 在不同 codec 下的可见性差异,验证它不进入序列化输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# /tmp/metadata_test.conf
input {
stdin {}
}

filter {
mutate {
add_field => {
"visible_field" => "this appears in output"
"[@metadata][hidden]" => "this should NOT appear"
}
}
}

output {
# rubydebug 会显示 @metadata(调试专用)
stdout {
codec => rubydebug { metadata => true }
}

# json codec 序列化结果:@metadata 不出现
file {
path => "/tmp/logstash_out.json"
codec => json_lines
}
}
1
2
bin/logstash -f /tmp/metadata_test.conf
# 输入:hello world

rubydebug 输出(含 metadata => true 选项):

1
2
3
4
5
6
7
8
9
10
{
"message" => "hello world",
"@timestamp" => 2026-08-06T13:20:00.000Z,
"@version" => "1",
"visible_field" => "this appears in output",
"@metadata" => {
"hidden" => "this should NOT appear",
"pipeline" => "main"
}
}

查看 /tmp/logstash_out.json(json_lines 输出):

1
{"message":"hello world","@timestamp":"2026-08-06T13:20:00.000Z","@version":"1","visible_field":"this appears in output"}

@metadata 不出现在文件输出中,与预期一致。

再演示 sprintf 字段引用:

1
2
3
4
5
6
7
output {
stdout {
codec => line {
format => "%{@timestamp} [%{[host][hostname]}] %{message}"
}
}
}

关键对象映射

event 概念 对应 Java/JRuby 类 说明
event 本体 org.logstash.Event 内部用 ConvertedMap 存储字段
@timestamp org.logstash.Timestamp 封装 java.time.Instant
@metadata ConvertedMap 子映射 encode 时不序列化
字段引用解析 FieldReference [a][b] 解析为路径数组
tag 操作 tags 字段(字符串数组) event.tag(x) 追加

模式提炼

event 的字段空间分三层:业务字段(随 event 输出)、系统保留字段(@timestamp@version,始终存在)、管道暂存区(@metadata,处理完成后剥离)。这种分层使 pipeline 内部的路由信息和处理状态不会污染最终的数据输出,是"关注点分离"在数据结构级别的体现。

@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][pipeline] 等少量内置字段,但 @metadata 主要是供用户在 filter 阶段存放中间状态,是主动设计的扩展点,而非被动元数据。

误解二:“@timestamp 就是 Logstash 收到日志的时间,直接用就行。”
实际情况:不经过 date filter 处理时,@timestamp 是 event 进入 input codec 的时刻,而非日志的原始发生时间。对于有历史回溯需求或时序分析的场景,必须用 date filter 从 message 中解析真实时间并覆盖 @timestamp

误解三:“字段引用 [host][host][hostname] 是一样的。”
实际情况:前者引用整个 host 对象(可能是 map),后者引用嵌套字段 host.hostname(字符串)。在条件表达式中混用会导致类型不匹配。

误解四:“rubydebug 输出就是最终写入 Elasticsearch 的内容。”
实际情况:rubydebug 默认包含 @metadata(加 metadata => true 选项时),而 Elasticsearch output 的 codec encode 会剥离 @metadata。两者内容不完全一致。

练习

  1. 编写一条 pipeline,从 stdin 读取 Apache access log 格式的文本(如 127.0.0.1 - - [06/Aug/2026:13:20:00 +0800] "GET / HTTP/1.1" 200 1234),用 grok filter 提取字段,再用 date filter 将提取的时间覆盖 @timestamp,最后用 rubydebug 输出,对比处理前后 @timestamp 的值。

  2. @metadata 中记录 filter 链经过的每个处理阶段名称(每经过一个 mutate 就追加一个标记),然后在 output 中用 if [@metadata][stages] 做条件分流,验证 @metadata 参与路由但不进入输出。

  3. ruby filter 调用 event.to_hash 打印所有业务字段的 key,观察 @metadata 是否出现在 to_hash 结果中。

系列导航

  • 上一篇:深入 Logstash 01 — 架构:JRuby、JVM 与 pipeline 的运行形态
  • 下一篇:深入 Logstash 03 — codec:字节流与 event 的边界转换

参考资料

  1. Elastic 官方文档 — Event API
  2. Elastic 官方文档 — Field References Deep Dive
  3. Elastic 官方文档 — Accessing Event Data and Fields in the Configuration
  4. Elastic 官方文档 — @metadata field
  5. Elastic 官方文档 — date filter plugin