深入 Logstash 03 — codec:字节流与 event 的边界转换
上一篇确立了 event 的内部结构:业务字段、系统保留字段与 @metadata 三层分离。这一篇进入 codec 层。codec 容易被误解成"就是格式化输出,和 filter 差不多"。更准确的说法是:codec 是字节流与 event 之间的边界转换器,解决的是"如何把连续字节流切割成离散 event"的成帧问题,这个问题在数据进入 filter 之前就必须解决,任何 filter 都无法替代。本文只抓一个问题:codec 与 filter 的分工边界在哪里,以及 multiline 为什么必须是 codec 而不是 filter。
数据流全景
1 | |
codec 的职责边界
codec 只做一件事:在字节流和 event 之间建立对应关系。具体地:
- decode 阶段(input 侧):从连续字节流中识别"一条记录"的边界,构造
Event对象,将内容写入message字段(plain/multiline)或直接展开为字段(json/json_lines)。 - encode 阶段(output 侧):将
Event对象序列化为字节流,写入目标。
codec 不读写 @metadata 以外的业务字段(json codec 除外——它直接将 JSON key 映射为 event 字段),不做条件判断,不做字段变换。这些属于 filter 的职责。
分工的本质差异:filter 在 event 粒度上操作,codec 在字节/帧粒度上操作。codec 的执行时机早于 filter,此时 event 边界尚未确定。
plain codec
最简单的 codec,也是默认 codec。行为:每读到一个换行符,产生一个 event,内容存入 message 字段。
1 | |
encode 方向:将 message 字段内容写出,追加换行符(默认)。
适用场景:单行日志、已知每行独立完整的文本格式。
json codec 与 json_lines codec
json codec:将整个输入(或输入的每个"块")解析为 JSON,字段直接成为 event 的顶层字段,无需 message 中转。适合 HTTP body、整块 JSON 文档输入场景。
json_lines codec:以换行符为分隔符,每行解析一个 JSON 对象。适合 ndjson(Newline Delimited JSON)格式,这是 Logstash 自身的 HTTP input 和很多现代日志框架的输出格式。
1 | |
两者的区别:json codec 遇到多行 JSON 文档时,需要整块数据一次性到达;json_lines 每读到换行即触发解析,更适合流式场景。
multiline codec:成帧决策的不可逆性
multiline codec 是理解"为什么 codec 不能由 filter 替代"的最佳切入点。
问题背景:Java 异常堆栈、Python traceback、多行 SQL 等日志,在物理上跨越多行,但语义上是一条完整记录。如果用 plain codec,每行产生一个 event,后续 filter 无法将它们重新合并——因为多个独立 event 之间没有关联,filter 的执行单位是单个 event。
multiline codec 的工作方式:
1 | |
配置参数:
| 参数 | 说明 |
|---|---|
pattern |
正则表达式,匹配"续行"特征 |
negate |
false = 匹配时续行;true = 不匹配时续行 |
what |
previous(追加到前一条)或 next(前置到后一条) |
max_lines |
单条 event 最多合并行数,防止内存耗尽 |
max_bytes |
单条 event 最大字节数 |
Java 异常堆栈的典型配置:
1 | |
为什么 multiline 必须是 codec,不能是 filter
这是一个架构约束,不是实现限制。
一旦字节流被 plain codec decode,每行已经成为独立的 Event 对象进入队列。队列中的 event 在语义上彼此独立,filter worker 每次取出一个 batch 处理,batch 内的 event 之间没有顺序保证(多 worker 并行时尤其如此)。即使用 filter 记录状态、尝试合并,也无法可靠地重建跨 event 的行序。
成帧(framing)必须在数据进入 event 模型之前完成。这正是 codec 存在的原因:在字节流还是连续序列的阶段,按规则切割边界,保证每个 Event 对象从诞生起就是语义完整的一条记录。
历史演进印证了这一点:Logstash 早期版本确实有 multiline filter,但它在多 worker 场景下会产生行序错乱和状态竞争问题,后来被废弃,功能移入 codec 层。当前 8.x 版本的推荐做法是在 Filebeat(数据来源侧)使用 multiline 配置,而非在 Logstash codec 层处理,原因同样是成帧应在最靠近数据来源的地方完成。
rubydebug codec
rubydebug 是 output-only codec,不能用于 input decode。作用是将 event 以 Ruby inspect 格式打印到标准输出,包含所有字段的类型信息,是 pipeline 调试的标准工具。
1 | |
生产环境不应将 rubydebug 作为 output 目标,它的 I/O 开销在高吞吐场景下会成为瓶颈。
可运行实验
目标:对比同一份多行 Java 异常堆栈,使用 plain codec(多 event)与 multiline codec(单 event)的处理差异。
准备测试数据文件 /tmp/stacktrace.log:
1 | |
用 plain codec(默认行为):
1 | |
输出:6 个独立 event,堆栈信息被割裂为 5 个独立 event。
用 multiline codec:
1 | |
输出:2 个 event。第一个 event 的 message 字段包含完整的异常堆栈(以 \n 连接),第二个 event 包含 INFO 行。
关键对象映射
| codec 概念 | 对应 Java/JRuby 类 | 说明 |
|---|---|---|
| codec 接口 | LogStash::Codecs::Base |
所有 codec 的基类,实现 decode/encode |
| plain | LogStash::Codecs::Plain |
按行切割,content → message |
| json | LogStash::Codecs::JSON |
Jackson 解析,字段直接展开 |
| json_lines | LogStash::Codecs::JSONLines |
行界 + JSON 解析 |
| multiline | LogStash::Codecs::Multiline |
状态机,维护未完成行缓冲 |
| rubydebug | LogStash::Codecs::RubyDebug |
output-only,调用 awesome_print |
模式提炼
codec 是"成帧层",filter 是"变换层"。成帧是不可逆操作:一旦字节流被切成离散 event,行序信息丢失,无法在 filter 层恢复。这个约束决定了 multiline 只能在 codec 层(或更上游的 Beats 侧)实现,是架构边界的直接体现,而非工程偏好。
codec 的另一个隐含职责是类型系统入口:json codec 保留 JSON 中的数字和布尔类型,plain codec 只产生字符串。选错 codec 会导致 filter 中的类型转换工作量倍增。
工程迁移表
| Logstash codec 概念 | Kafka SerDe 对应 | 网络协议对应 | HTTP 对应 |
|---|---|---|---|
| codec(成帧+反序列化) | Deserializer(Consumer 侧) | 协议成帧(TCP framing) | Content-Type 解码 |
| plain decode | StringDeserializer | 行界符分帧 | text/plain 读取 |
| json decode | JsonDeserializer | 消息边界固定 | application/json 解析 |
| multiline decode | 无直接对应(Producer 侧已完成) | 长度前缀 / 分隔符成帧 | multipart body |
| codec encode(output) | Serializer(Producer 侧) | 协议编码 | Content-Type 编码 |
| rubydebug | 无对应(调试打印) | 无对应 | 无对应 |
常见误解
误解一:“codec 只是格式化,随时可以换,和 filter 没本质区别。”
实际情况:codec 决定 event 的边界和初始字段结构;错误的 codec 选择(如对多行日志使用 plain)会使 filter 阶段的数据完全无法正确处理,无法通过增加 filter 补救。
误解二:“multiline filter 可以合并多行,multiline codec 是多余的。”
实际情况:Logstash 8.x 中 multiline filter 已被废弃,且在多 worker 场景下存在行序竞争问题。multiline codec 在 input 线程(单线程)内完成合并,不存在并发问题。
误解三:“json codec 和 json_lines codec 可以互换。”
实际情况:json codec 期望整个输入块是一个完整的 JSON 文档;json_lines 以换行符为分隔符逐行解析。对 ndjson 流用 json codec 会导致解析失败或只读取第一行。
误解四:“rubydebug 输出可以用于生产日志采集回路。”
实际情况:rubydebug 使用 awesome_print 库生成人类可读格式,I/O 开销显著高于 json_lines,且输出格式不是机器可解析的标准格式,不适合生产场景。
练习
-
准备一份包含多行 Python traceback 的日志文件,分别用 plain codec 和 multiline codec 处理,对比输出 event 数量和
message字段内容;调整 multiline 的pattern和negate参数,找到正确匹配 Python traceback 续行的配置。 -
用 json_lines codec 处理以下混合输入(一行合法 JSON + 一行非法 JSON),观察 Logstash 对解析失败行的处理方式(是否产生
_jsonparsefailuretag,原始内容是否保留在message中):1
2
3{"level":"info","msg":"started"}
this is not json
{"level":"error","msg":"failed"} -
在同一条 pipeline 中,input 侧用 json_lines codec,output 侧分别试用 json_lines 和 plain codec,对比两种 output codec 序列化后的字节内容差异;重点观察嵌套字段在 plain codec 下如何处理。
系列导航
- 上一篇:深入 Logstash 02 — event 模型:@timestamp、@metadata 与字段引用
- 下一篇:深入 Logstash 04 — Grok:非结构化文本的字段提取
参考资料
- Elastic 官方文档 — Codec Plugins
- Elastic 官方文档 — multiline codec plugin
- Elastic 官方文档 — Managing Multiline Events
- Elastic 官方文档 — json codec plugin
- Filebeat 官方文档 — Multiline messages
