资讯动态

Filebeat JSON日志处理:字段裁剪、扁平化与拆分实操指南

发布时间:2026/10/24 3:10:21 来源:尧图企业网站定制
维护日志链路的朋友大概率都遇到过这种场景应用明明打的是标准JSON到了Kibana里一看有的字段变成了一长串嵌套路径有的关键字段被Filebeat自带字段覆盖了还有一堆调试用的冗余字段把索引空间撑得很大。标题里拆出来的本质上是三个需求去掉JSON日志里的部分属性、把嵌套JSON拍平、按业务需求拆分JSON里的内容。这篇就围绕这三个问题用Filebeat完整走一遍配置、排错和落地思路。先说清楚这篇内容解决什么不依赖Logstash、不写ES端复杂管道尽可能用Filebeat自带的解析能力和处理器processor完成JSON日志的采集、字段裁剪、扁平化与拆分。适合正在用Elastic Stack做日志平台、被同事吐槽“Kibana里字段没法看”的开发者也适合刚接手Filebeat、想知道json.keys_under_root和decode_json_fields到底怎么选的新手。1. 项目概述与需求拆解1.1 标题背后的三个核心需求表面看这只是“Filebeat采集JSON日志”但结合实际运维场景诉求其实非常具体。第一是“去掉部分属性”。业务日志里经常混入大量不必要的内容调试用的堆栈、内部上下文字段、请求链路里的大对象。如果全部写入Elasticsearch存储成本会涨索引mapping也会被这些脏字段撑出一堆动态映射。所以要做字段裁剪。第二是“JSON日志扁平化”。默认情况下Filebeat把JSON解析后如果内容里还有嵌套对象进入ES后会形成嵌套结构比如context.orderId、context.userId。这带来的问题是查询时要写很长路径Kibana可视化里字段展示很别扭Painless脚本里取字段也要写完整路径。把嵌套路径拍平成orderId、userId这类顶层字段就是所谓的扁平化。第三是“拆分JSON日志”。这个词最容易产生歧义。一种理解是把嵌套对象里的字段拆出来变成独立顶层字段这其实和扁平化是一回事另一种理解是日志内容里有一个数组数组里每个元素代表一条独立业务事件要把一条日志拆成多条记录。这两种“拆分”的实现方式完全不同后面会分别说清楚。1.2 为什么这件事要用Filebeat做日志从产生到进入Elasticsearch一共有四个可以干预处理的位置应用自身、Filebeat、Logstash、ES的Ingest Pipeline。应用自身处理是最理想的但现实很骨感日志格式由业务代码决定采集链路的维护者往往改不动别人的代码或者改一发版要排很久。Logstash虽然处理能力强但中间多一跳组件部署、监控、维护都多一摊事。ES的Ingest Pipeline要占ES节点CPU日志量一大容易拖慢写入。Filebeat的优势在于资源占用极小配置灵活自带的processor已经能覆盖绝大多数JSON处理需求。对中小规模业务来说直接在Filebeat里把解码、裁剪、扁平化都做完是性价比最高的方案数据在传输前就瘦身了带宽和存储都省。1.3 明确处理链路与输出目标为了后面演示不跑偏先把目标链路定下来Filebeat读取特定目录下的日志文件按行解析JSON删除明确不需要的冗余字段对嵌套对象做扁平化将业务时间戳转换为timestamp最后输出到Elasticsearch。整条链路只依赖Filebeat本身不引入Logstash。这个链路里最关键的是搞清楚每个处理动作的顺序因为processor是按顺序执行的前一个的输出就是后一个的输入。顺序错了结果会千奇百怪。这个点我会在后面的实战配置里反复强调。2. Filebeat环境与配置基础2.1 filebeat.yml的配置层级展开配置之前先把Filebeat的配置模型讲清楚。filebeat.yml里四个核心层级filebeat.inputs定义从哪读、parsers或input配置项定义怎么解析、processors定义事件进入输出之前怎么加工、output定义最终发到哪。一个不错的类比是工厂流水线input是原料入口parser是拆箱机processor是流水线上的一组工位去杂质、贴标签、改包装output是出货口。每个工位按顺序工作前面工位的产物就是后面工位的输入。2.2 两种JSON解码方式对比Filebeat处理JSON日志常见的解码方式有三种很多人一上来就混用导致结果莫名其妙。第一种是log input的json配置项经典但偏老。配置写法是filebeat.inputs: - type: log paths: - /var/log/app/*.log json.keys_under_root: true json.overwrite_keys: true json.add_error_key: true json.message_key: messagejson.keys_under_root: true表示把JSON最外层字段直接放到事件的根级别json.overwrite_keys: true表示如果业务JSON里有字段和Filebeat自带字段同名允许覆盖json.add_error_key: true表示解析失败时加一个error字段便于排查。第二种是filestream input的ndjson parser这是新版本推荐的方式filebeat.inputs: - type: filestream id: app-json-logs paths: - /var/log/app/*.log parsers: - ndjson: target: overwrite_keys: true add_error_key: truefilestream input是Filebeat 7.x开始重点发展的事件流式采集方式但它不支持log input的json.*配置改用parsers里的ndjson。target设为空字符串效果等同于json.keys_under_root: true。第三种是用decode_json_fields processor这是最灵活的方式也是我比较推荐的方式processors: - decode_json_fields: fields: [message] target: overwrite_keys: true它做的事情是把事件里message字段的内容当作JSON解析然后按target参数决定放哪里。与input级json配置最大的区别是decode_json_fields是processor可以和其他processor任意组合也可以用条件判断控制它只对某些事件生效。三种方式的取舍用表格看更直观对比项log input的json配置filestream的ndjson parserdecode_json_fields processor适用inputlogfilestream两者都可用作用时机input读取阶段input读取阶段事件处理阶段能否与其他processor自由组合受限受限可以能否对部分事件生效否否可以版本趋势老项目常见新项目推荐通用实际项目中我的习惯是老版本存量配置用json.keys_under_root不动它新写的配置统一用filestream input加decode_json_fields processor这样处理逻辑都集中在一个地方排查问题不用翻好几个配置段。2.3 processor执行顺序的关键性processors数组里的顺序就是执行顺序这一点极其重要。举个例子如果你先做drop_fields删掉message字段再做decode_json_fields去解message这时message已经没了decode会跳过或报错。反过来先decode把message里的内容解析成字段再drop_fields删掉冗余字段逻辑才是通的。我在真实项目里见过最典型的错误是在decode_json_fields之前加了一个rename把message改名了结果decode_json_fields找不到字段整个JSON解析静默失败原始字符串原封不动进了ES。排查流程走了好久最后发现就是一行配置顺序的问题。2.4 字段保留策略的隐藏风险当JSON字段被放到事件根级别后事件里会同时存在三类字段Filebeat内部字段timestamp、metadata、host、log、agent等、业务字段、可能存在的ECS字段。做字段裁剪前必须清楚哪些字段是ES和Kibana运行依赖的。比如include_fields白名单如果fields列表里忘了timestamp那么timestamp会在处理中被过滤掉日志写入ES后时间轴直接乱掉。这个坑很多人踩过而且排查难度高因为数据能进ES看起来一切正常就是Kibana时间不对。做白名单保留时默认把timestamp、log、host这类系统字段带上再删业务字段会稳妥很多。3. JSON日志扁平化核心实操3.1 为什么日志要扁平化拿一条实际日志举例原始JSON长这样{ timestamp: 2024-01-15T10:30:00Z, level: INFO, logger: OrderService, message: 订单创建成功, context: { orderId: ORD-12345, userId: 10086 } }如果不做扁平化context整体会作为一个嵌套对象进入ESKibana字段列表里看到的就是context.orderId、context.userId查询要写完整点号路径建看板时要一层层展开。如果业务里嵌套三层四层mapping会越来越乱动态映射产生的字段也会暴涨。扁平化的目标是让context.orderId变成orderId这样的顶层字段或者至少变成带前缀的扁平字段让查询和展示都直观。3.2 target: 实现的是“浅扁平化”很多人误以为decode_json_fields的target设为空字符串就能把所有嵌套层级都拍平。实际上target: 只是把JSON最外层的字段提升到事件根级别。打个比方它拆掉了外包装箱但里面的小盒子还在。对于只有一层嵌套的场景例如JSON里所有字段本来就在顶层那target: 确实够用。但如果JSON里本身有嵌套对象比如context字段下还有orderIdtarget: 之后context还是嵌套对象并没有真正展开。这是“浅扁平化”和“深度扁平化”的区别。3.3 深度扁平化script processor递归展平要实现真正的深度扁平化Filebeat的script processor可以派上用场。这个processor支持JavaScript可以对事件做任意操作。下面是我在项目里验证过的一段递归展平脚本processors: - script: lang: javascript source: function flattenNode(node, prefix, output) { for (var key in node) { if (!node.hasOwnProperty(key)) continue; var fullKey prefix ? prefix _ key : key; var value node[key]; if (value ! null typeof value object !Array.isArray(value)) { flattenNode(value, fullKey, output); } else { output[fullKey] value; } } } function process(event) { var doc event.Get(); if (doc[context] ! undefined) { var flat {}; flattenNode(doc[context], ctx, flat); for (var k in flat) { event.Put(k, flat[k]); } event.Delete(context); } }这段脚本的意图是把context对象里的所有嵌套字段递归展开新字段名用下划线拼接比如context.orderId变成ctx_orderIdcontext.userId变成ctx_userId然后把原始的context字段删除。注意Filebeat script processor的JavaScript API和Elasticsearch的script processor不一样这里用的event.Get、event.Put、event.Delete是Filebeat事件对象的原生方法。写脚本时先看对应版本的文档不同大版本之间API可能有调整。3.4 关于扁平化字段名的关键认知这里有个特别容易踩的知识点如果你想拍平字段新字段名里千万别带点号。比如把context.orderId改名为context.orderId还是原来那个名字这在ES里依然会被当作嵌套路径处理等于白干。真正要做到扁平化字段名里不能有点号分隔符要么用下划线要么用自定义前缀。这个道理类似文件路径a/b/c是路径分隔a_b_c就是一个普通文件名。ES的字段名机制也一样点号就是路径分隔符。理解了这一点你就知道为什么很多人“扁平化”之后Kibana里还是嵌套结构。3.5 扁平化后的字段名冲突处理扁平化时最常见的冲突是两条嵌套路径展平后使用了同一个字段名。例如context.orderId和data.orderId如果都直接展平成orderId后写的事件会覆盖先写的字段。稳妥的做法是带上前缀。我上面的脚本生成ctx_orderId就是把所有字段都加了ctx前缀这样从context里出来的字段天然不会和日志顶层的orderId冲突。如果业务上有多个嵌套对象需要展开建议分别为它们设置不同的前缀干脆避免冲突别指望ES能帮你智能合并。4. 去掉部分属性三种方案的对比与选型4.1 黑名单方案drop_fields直接删最简单的裁剪手段是drop_fields写法很直接processors: - drop_fields: fields: [extra_verbose_data, context.items] ignore_missing: trueignore_missing: true表示字段不存在时不会报错这在处理多格式日志时很重要不然某个字段在这个事件里没有整个processor会报异常。这个方案的适用场景是字段总量可控你明确知道哪些字段是多余的。比如日志里有一个巨大的extra_verbose调试字段直接删掉最省事。需要注意drop_fields删嵌套字段时要写的是事件里的实际路径。如果前面已经做了扁平化原来的context.orderId已经变成ctx_orderId了这时候再写context.orderId肯定删不到东西。4.2 白名单方案include_fields只留需要的字段特别多、真正用的没几个时相反的做法更高效只保留关心的字段。processors: - include_fields: fields: [timestamp, log, host, level, logger, message, ctx_orderId, ctx_userId]白名单的好处是业务日志里以后新增了字段也不会自动进入ES索引mapping非常干净不会出现动态字段泛滥。代价是每次想看一个新字段都要改配置灵活性差一些。这里我必须再次强调include_fields的列表一定要带上timestamp、log、host这些系统字段。我自己就吃过亏第一次配白名单时只想着留业务字段结果时间字段被过滤掉了Kibana里所有日志的时间全乱了排查了很久才定位到是这个原因。4.3 服务端方案ES Ingest Pipeline如果处理逻辑希望集中在ES端可以在Elasticsearch里建一个ingest pipelinePUT _ingest/pipeline/app-log-json { processors: [ { remove: { field: extra_verbose_data } }, { rename: { field: context.orderId, target_field: ctx_orderId } } ] }然后在Filebeat的output里引用output.elasticsearch: hosts: [http://localhost:9200] index: order-app-%{yyyy.MM.dd} pipeline: app-log-json这个方案的优势在于逻辑集中在ES端多个采集器过来都能复用同一套处理规则改一次全链路生效。劣势也明显冗余字段已经通过网络传到了ES白白占用带宽而且ingest节点在高峰期的CPU开销不可忽视。4.4 方案对比与选型建议方案执行位置节省传输带宽新增字段自动保留适合场景drop_fieldsFilebeat是是明确知道要删哪些字段include_fieldsFilebeat是否需改配置字段极多且只需要少数几个ES Ingest PipelineES否是多采集器复用同一套规则我的经验是中小规模日志链路优先全用Filebeat处理。先decode再drop_fields删明确冗余的字段如果字段确实太杂改用include_fields白名单但一定把系统字段带上。单日志量非常大的场景建议把复杂的字段重命名和类型转换放到ES pipeline里Filebeat只做最基础的解码和裁剪降低Beat端的CPU压力。5. JSON日志拆分字段拆分与事件拆分5.1 先搞清楚“拆分”指什么“拆分JSON日志”这个词确实容易让人犯迷糊。我基于实际需求把它拆成两类。第一类是字段拆分把嵌套对象里的字段拆出来变成顶层字段。比如context里的orderId拆成ctx_orderId这个本质上就是前文讲的扁平化只是换了种说法。第二类是事件拆分日志里有一个JSON数组数组中每个元素代表一条独立业务事件希望一条日志变成多条记录进入ES。比如订单日志里orders数组包含多笔订单你想让每笔订单成为一条独立日志。这两种需求的技术路径完全不同下面分开讲。5.2 字段拆分的实现思路字段拆分完全可以在Filebeat内用script processor完成。比如下面这段脚本把orders数组里每个元素展开成order_0_id、order_0_amount这样的字段processors: - script: lang: javascript source: function process(event) { var orders event.Get(orders); if (orders Array.isArray(orders)) { for (var i 0; i orders.length; i) { event.Put(order_ i _id, orders[i].orderId); event.Put(order_ i _amount, orders[i].amount); } event.Delete(orders); } }这样做的前提是数组长度相对固定。因为数组一旦变成order_0_id、order_1_id这种固定字段ES的mapping会随数组长度动态增加字段如果长度不可控mapping会很难看字段会无限膨胀。所以字段拆分只适合数组长度有限且稳定的场景。5.3 事件拆分的正确姿势Filebeat加Logstash把一条日志拆成多条记录Filebeat本身没有这个能力这是它的边界。要完成事件拆分标准方案是Filebeat把原日志原样发给Logstash由Logstash的split filter处理。Logstash配置示例input { beats { port 5044 } } filter { json { source message target body } split { field [body][orders] target order } mutate { remove_field [body, message] } } output { elasticsearch { hosts [http://localhost:9200] index order-detail-%{yyyy.MM.dd} } }split filter的工作原理可以这样理解父事件进来了orders数组有几个元素它就复制出几个事件副本每个副本里把orders数组的某个元素赋值到order字段上同时保留父事件的其他字段。最后用mutate把body、message这些中间字段删掉输出的就是干净的独立订单事件。这里有一个业务上的取舍事件拆分后每条日志携带的上下文比如请求ID、用户ID会被复制到每条记录中这会让存储膨胀但查询体验好很多。如果业务上查询单笔订单时不想关联回父日志这个代价是值得的。5.4 拆分后的索引设计建议事件拆分后数据量通常比原来多好几倍。写入ES时要注意两点一是给拆分后的事件设置合理的索引分片数不要沿用父日志的索引配置二是在输出端尽量让ES做幂等写入用业务订单号做文档ID避免重复采集时产生重复文档。如果不想为拆分额外维护一套Logstash也可以在应用端直接修改日志结构把数组在日志产生时就打成多条独立JSON行。这个方案最干净但前提是你能改业务代码。6. 完整实战配置一个可复现的Demo6.1 演示日志样例为了把配置讲具体模拟一份订单应用日志存到/var/log/app/order-app.log{timestamp: 2024-01-15T10:30:00Z, level: INFO, logger: OrderService, message: 订单创建成功, context: {orderId: ORD-12345, userId: 10086}, items: [SKU001, SKU002], extra_verbose: {traceId: trace-01, stack: ...}}这份日志包含了我们要演示的所有处理点timestamp需要转成timestampcontext需要扁平化extra_verbose需要删除items数组要保持原样。6.2 filebeat.yml完整配置下面是一份可直接复用的完整配置filebeat.inputs: - type: filestream id: order-app-log paths: - /var/log/app/order-app.log parsers: - ndjson: target: overwrite_keys: true add_error_key: true processors: - timestamp: field: timestamp layouts: - 2006-01-02T15:04:05Z - 2006-01-02T15:04:05.999Z timezone: Asia/Shanghai ignore_failure: true - drop_fields: fields: [extra_verbose] ignore_missing: true - script: lang: javascript source: function flattenNode(node, prefix, output) { for (var key in node) { if (!node.hasOwnProperty(key)) continue; var fullKey prefix ? prefix _ key : key; var value node[key]; if (value ! null typeof value object !Array.isArray(value)) { flattenNode(value, fullKey, output); } else { output[fullKey] value; } } } function process(event) { var doc event.Get(); if (doc[context] ! undefined) { var flat {}; flattenNode(doc[context], ctx, flat); for (var k in flat) { event.Put(k, flat[k]); } event.Delete(context); } } output.elasticsearch: hosts: [http://localhost:9200] index: order-app-%{yyyy.MM.dd}配置里要解释几个关键点。timestamp processor的layouts用的是Go时间布局格式不是Java的yyyy-MM-dd格式。Go格式里2006代表年、01代表月、02代表日、15代表小时、04代表分钟、05代表秒。这是Filebeat和Logstash时间格式最大的不同很多人第一次用都会在这里翻车。ignore_failure: true的含义是时间解析失败时processor不会让整个事件处理中断而是继续往下走。这在实际生产环境里很有用因为偶尔会出现一条格式异常的日志你不能因为一条脏数据把整批事件都堵住。drop_fields放在timestamp之后没有依赖关系但放在decode之后是关键因为extra_verbose是解析后的字段不是原始日志行里的路径。script processor的flattenNode递归函数会生成ctx_orderId、ctx_userId这些带前缀的扁平字段然后删除原始的context对象。如果context里还有更深层的嵌套这个递归函数会继续往下展开直到所有非对象字段都变成顶层字段。6.3 启动与验证步骤写完配置先别急着上生产本地跑通再说。三个命令很实用filebeat test config -c filebeat.yml filebeat test output -c filebeat.yml filebeat -e -c filebeat.yml第一条检查配置语法第二条检查output连接是否可用第三条前台运行并输出日志。如果处理逻辑复杂我习惯先把output改成console做本地调试output.console: pretty: true这样可以直观看到每个事件经过processors之后长什么样确认字段都对了再把output切回elasticsearch。这个调试手法帮我省了大量去Kibana刷index的时间。6.4 验证结果与字段对比经过上面配置处理后原始日志字段会变成这样原始字段路径处理后字段处理方式timestamptimestamp、timestamptimestamp processor转换context.orderIdctx_orderIdscript processor扁平化context.userIdctx_userIdscript processor扁平化extra_verbose已删除drop_fieldsitemsitems原样保留level、logger、message原样保留根级字段在Kibana的Discover页面里如果索引模板已经生成打开字段列表就能看到ctx_orderId、ctx_userId这些顶层字段不再有context这个嵌套对象。items作为数组字段保留方便后续做聚合分析。7. 常见问题与排查技巧实录7.1 日志解析失败却没有任何报错最常见的现象是原始日志明明是JSON写入ES后message字段还是完整字符串或者干脆多了一个error字段。原因通常是add_error_key没开启解析失败的信息被Filebeat吞掉了。排查时先做的不是改配置而是用console output直接看Filebeat内部事件长什么样output.console: pretty: true如果事件里的message还是原始字符串说明decode_json_fields根本没匹配到那个字段检查fields写的对不对。如果事件里有很多error字段说明JSON解析失败把日志原文复制出来用格式化工具检查一下多半是日志里混了多行JSON或者非JSON文本。7.2 时间字段被覆盖或丢失Kibana日志时间全是Filebeat采集时刻的实时时间而不是业务日志本身的时间。这个问题的根源是没有配置timestamp processor或者配置了但field写错了又或者字段在include_fields时被误删。把业务时间字段转成timestamp是JSON日志处理的标准动作。同时记得如果业务字段里也有一个timestamp或类似字段decode时overwrite_keys开启后业务字段也可能反过来覆盖系统时间导致时间错乱。这种情况建议把业务时间字段rename成一个业务专属名比如event_time再用timestamp processor转系统时间。7.3 嵌套字段依然存在扁平化没生效我在实际项目中经历过这种场景按照网上教程配置了decode_json_fields的target: 以为能完全展平结果context还是嵌套在ES里。原因前文已经说过target: 只是拆外包装要完全展开必须用递归脚本。还有一个隐蔽问题如果脚本生成的字段名里带了点号比如直接写成ctx.orderId那么ES仍然会把它当作嵌套路径。字段名里只要有点号在ES里就是层级结构。检查脚本生成的字段名确认没有点号。7.4 processor顺序导致的诡异结果processor顺序的问题是排查成本最高的因为很多错误很隐蔽。比如先做了include_fields把message以外的业务字段全删了然后decode_json_fields再去解析message解析出来的字段当然也没了因为事件只剩下message一个字段。经验法则是先做decode再做rename再做字段裁剪再做时间转换最后做输出相关处理。按这个顺序排能避开80%的processor顺序问题。7.5 script processor的性能隐患最后讲一个很多人忽略的点script processor用的是JavaScript引擎解释执行开销远高于原生processor。日志量小无所谓但一天几亿条日志时如果每条都要跑一段递归展平脚本Filebeat的CPU会明显吃紧。我踩过这个坑之后的实践原则是能用原生processordecode_json_fields、drop_fields、rename、timestamp解决的绝不写脚本脚本只处理真正无法用原生能力完成的逻辑。如果日志量真的很大把复杂处理尽量往ES Ingest Pipeline或Logstash移让Filebeat只做最轻量的采集和裁剪。还有一种更彻底的思路找业务方沟通让他们在输出日志前就把JSON结构拍平。比如日志里直接输出orderId而不是输出context对象让采集端再去拆。很多团队绕了一大圈做扁平化最后发现让业务改一下日志结构比任何采集端处理都干净高效。我这里多分享一句个人体会Filebeat的processor能力很强但不代表所有事情都适合在Beat端做。学会判断什么该在Filebeat做、什么该给Logstash、什么该留给ES pipeline比背一堆配置参数重要得多。日志链路每多一个处理环节排查链路就多一分复杂度能用最简单方案解决的别为了炫技堆复杂处理。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑