最近给几个服务的日志做了一次集中治理,第一件事就是把 JSON 日志里那些没用的字段处理干净。说实话,Filebeat 采集 JSON 日志这件事看着简单,实际配置起来坑不少,尤其三个高频需求——去掉部分 JSON 属性、把嵌套 JSON 扁平化、把一条 JSON 日志拆成多条事件——每个都有人天天在群里问。这篇就把我实际用elastic/filebeat处理这几类问题的配置、思路和踩过的坑完整写出来,适合正在做日志采集、日志治理或 Elasticsearch 数据清洗的同学直接参考。
先说清楚这篇能解决什么问题:如果你手里有一堆 JSON 日志,里面塞满了无用的上下文信息,或者嵌套结构太深导致查询很痛苦,又或者一条日志里带一个数组想拆成多条明细事件,那么下面的内容就是为你准备的。我会从需求本质讲起,落到完整的filebeat.yml配置,再附上生产环境实测的排障经验,配置可以直接抄。
1. 先理清这三种需求的本质
1.1 为什么非要在采集端处理JSON日志
很多人第一反应是:JSON 日志采集上来,直接把原始内容丢进 Elasticsearch 不就行了?Field 多就多,反正 ES 是文档型数据库。这个想法在日志量小的时候没问题,一旦上了规模,代价立刻显现。
首先是存储成本。ES 的存储成本和字段数量强相关,尤其是开启了 text 类型或者启用了 doc_values 的字段,多一个字段就多一分索引开销。我经手过一个业务系统,单条日志原本有 40 多个字段,经过治理砍到 12 个核心字段,同样的日志量,索引体积直接降了将近一半。这个数字在 GB 级、TB 级日志场景下,对应的可能就是真金白银的磁盘和集群资源。
其次是查询效率。字段越多,Kibana 里 Discovery 页面越乱,用户找关键字段越费劲。嵌套层级深了之后,写查询语句也容易出错,比如user.address.city这种三层路径,在 ES 里稍不留神就会把 map 类型和 text 类型搞混,出现 mapping 冲突。
最后是安全合规。日志里经常混着手机号、身份证号、token、密码哈希甚至明文密码。如果不在采集端做一次清洗,这些敏感数据就会原样进入 ES,一旦索引权限配置不当或者导出日志,就是直接的数据泄露。所以在采集端把敏感字段摘掉或者替换掉,是日志治理的第一道防线,也是成本最低的一道防线。
那为什么选 Filebeat 而不是 Logstash 或者 ES Ingest Pipeline?Filebeat 是部署在业务机器上的轻量采集器,在数据源头就把问题解决掉,意味着后面所有环节拿到的都是干净数据。相比 Logstash,Filebeat 内存占用小得多,适合大规模部署;相比 ES Ingest Pipeline,Filebeat 的处理不消耗 ES 集群的 CPU,而且日志还没进网络就已经变小了,传输压力也随之下降。
1.2 裁剪、扁平化、拆分到底在解决什么问题
这三个需求看着是一件事,其实本质完全不同。
去掉部分属性,解决的是“数据太多”的问题。典型来源是框架自动打印的上下文:日志级别、线程名、类名、请求路径、用户代理、环境变量,甚至一次请求打出的完整请求头和响应体。这些字段大多时候没有分析价值,留着只会在 ES 里占空间、拖慢查询。所以要去掉无关属性,尤其是敏感属性。
扁平化,解决的是“结构太深”的问题。打个比方,你收到的 JSON 是{"user": {"name": "张三", "profile": {"age": 30}}},如果原样入库,Kibana 里看这个字段要一层层点开,每次写查询都得记路径。但是扁平化之后,变成user_name: 张三、user_profile_age: 30这种平铺的键,查询、聚合、导出 CSV 都舒服得多。很多数据分析场景需要把数据导出成表格格式,嵌套结构基本没法直接导出,扁平化是刚需。
拆分,解决的是“粒度不匹配”的问题。最典型的场景是一次批量操作日志里带一个明细数组,比如“用户批量下单”日志里含orderList: [{orderId: "A"}, {orderId: "B"}]。这时候只有两条路:要么把整个数组当成一个字段存进 ES,那么你没法按单个订单去聚合、告警、关联;要么在采集端把这个数组拆开,一条日志变成两条事件,每个事件代表一个订单,同时保留外层日志里的公共字段(比如 traceId、userId)用于关联。拆分的价值就是把日志的分析粒度细化到业务最小单元。
1.3 决定在Filebeat做还是扔给下游做
我见过不少团队在这个选择上反复折腾,最后总结下来其实就一句话:通用、简单、对资源敏感的清洗放在 Filebeat,复杂、需要上下文富化的解析放到 ES Ingest Pipeline 或者 Logstash。
做一个 30 秒就能想明白的判断。
| 判断维度 | 适合在 Filebeat 做 | 适合丢给下游 |
|---|---|---|
| 处理逻辑 | 删字段、改字段名、简单解析 JSON | 需要正则提取、Grok 解析、GeoIP 地理位置富化 |
| 依赖数据 | 只依赖当前事件本身 | 需要查参考数据、关联其他索引 |
| 资源消耗 | 尽量轻量,避免大面积 script | 允许中等计算量 |
| 变更频率 | 低,变一次就全量部署 | 高,可在 ES 侧热更新 |
这个表格看着简单,实际操作中有个隐含问题:Filebeat 的 processor 一旦写复杂了,出了问题要重新打包部署到每台业务机,周期长、影响面大。而 ES Ingest Pipeline 的更改是热更新的,直接在 Kibana 里改完就生效。所以我的习惯是:采集端只做“减法”和“结构化”两类操作,其余一律往后放。
2. Filebeat处理JSON的底层逻辑与关键配置
2.1 json解码配置:keys_under_root、overwrite_keys与错误兜底
Filebeat 采集 JSON 日志,最基础的一步是把文本变成结构化字段。这里有一个高频误区:很多人不知道 Filebeat 有两种完全不同的 JSON 解析路径。
第一种是整行日志本身就是 JSON。这种情况直接在 input 层配置json项。新版 Filebeat 推荐用filestreaminput,配合parsers里的ndjson;老版本用loginput,直接在 input 下配置json.*。两种本质是一样的,只是写法位置不同。
filebeat.inputs: - type: filestream id: app-json-log enabled: true paths: - /data/logs/app/*.log parsers: - ndjson: keys_under_root: true overwrite_keys: true add_error_key: true ignore_decoding_error: true用旧的loginput 则是这样:
filebeat.inputs: - type: log enabled: true paths: - /data/logs/app/*.log json.keys_under_root: true json.overwrite_keys: true json.add_error_key: true json.ignore_decoding_error: true这里四个参数每一个都有讲究。
keys_under_root决定解析出来的字段放在哪。设为true,JSON 里的字段直接平铺到事件根级别;设为false(默认),所有解析结果塞在json字段下面,形成一层嵌套。绝大多数人希望日志字段直接可见,所以设成true。
overwrite_keys决定解析出的字段能不能覆盖 Filebeat 自己的元数据字段。比如你的 JSON 里恰好有个字段叫hostname,或者叫agent,如果不开 overwrite,Filebeat 自带的同名信息会保留,你的日志字段就被藏起来了;开了之后,日志字段会覆盖掉系统字段。这个要谨慎,因为覆盖了hostname之类的字段可能导致你后面想按机器维度聚合都做不到。我的经验是:如果日志字段名和系统字段名有冲突,优先在日志源头改字段名,而不是盲目打开 overwrite。
add_error_key会在解析失败的事件里加入一个error.message字段,方便你排查哪行日志不是合法 JSON。
ignore_decoding_error,我强烈建议生产环境设为true。否则一旦某条日志因为特殊字符导致解析失败,轻则记录错误日志,重则事件处理异常,影响整条采集链路。设为true之后,解析失败的行会以原始字符串姿势进入后续流程,你再通过error.message来筛就行。
第二种是日志本身是普通文本,但其中一个字段的值是一个 JSON 字符串。典型场景是应用打了{"timestamp": "...", "context": "{\"userId\":123}"}这种日志。这种情况光靠 input 层的 json 解析不够,需要在 processors 里用decode_json_fields把那个 JSON 字符串字段再解一层。这个后面实战部分会展开。
2.2 processors机制:写配置之前必须知道的执行顺序
Filebeat 的 processors 是一个按顺序执行的处理链。每一条日志事件进入之后,会从头到尾过一遍 processors 列表,前一个处理器的输出是后一个的输入。这个顺序极其重要,因为处理逻辑是分阶段的:先解析、再变形、再删除,顺序反了会导致字段还没生成就找不到路径。
processors: - decode_json_fields: fields: ["message"] target: "" overwrite_keys: true - flatten: field: data separator: "_" ignore_missing: true - drop_fields: fields: ["message"]上面这个顺序就很典型:先用decode_json_fields把 message 里的 JSON 字符串变成真实字段,然后对data对象做扁平化,最后删掉已经没用的原始message字段。如果你把drop_fields放到最前面,message字段被删了,后面 decode 找不到源字段,整个逻辑就废了。
还有两个参数容易忽略:ignore_missing和fail_on_error。前者表示如果源字段不存在,要不要默默跳过;后者表示如果处理器出错,要不要丢弃这条事件。Java 里做 NPE 防护一样,生产环境的日志千奇百怪,不是每条都有你预期的字段结构。我的习惯是处理 JSON 的 processor 一律把ignore_missing设为true,fail_on_error除非特殊要求,否则保持false,让单条日志的问题只影响它自己,不要拖累整个采集流。
2.3 处理JSON的几把常用“工具”语法速查
我整理了一份自己常用的 processor 语法,都是处理 JSON 日志最高频的几把工具。
# 把字段里的 JSON 字符串解析成结构化对象 - decode_json_fields: fields: ["message"] process_array: false max_depth: 2 target: "" overwrite_keys: true add_error_key: false # 删除指定字段 - drop_fields: fields: ["log.level", "trace_id"] ignore_missing: true # 字段重命名 - rename: fields: - from: "user.name" to: "user_name" ignore_missing: true fail_on_error: false # 把嵌套对象拍平成一层 - flatten: field: data separator: "_" ignore_missing: true # 把数组字段拆成多条事件 - split: field: items target: item keep_parent: true几个细节说明。decode_json_fields的max_depth默认只有 1,意味着它只会解析第一层嵌套,再往深的 JSON 字符串不会被递归展开。如果你的 message 字段里嵌套了三层 JSON 字符串,必须把max_depth调大,或者先解析成对象之后用flatten拍平,否则你会在 ES 里看到一堆字符串类型字段,跟没解析一样。
flatten是 7.10 左右加入的处理器(具体版本看官方 changelog),如果你的 Filebeat 版本太老没有这个处理器,可以用rename挨个字段改名,或者写一个script处理器手动展开。新版生产环境基本都有,放心用。
split的keep_parent尤其关键,后面实战会重点讲。
3. 三种实战场景的完整filebeat.yml写法
3.1 场景一:剥离冗余与敏感字段
这个场景我遇到最多。比如某服务的日志长这样:
{"timestamp": "2024-01-15T10:00:00Z", "level": "INFO", "class": "OrderServiceImpl", "method": "createOrder", "traceId": "abc123", "userId": 10001, "requestBody": {"password": "123456", "token": "eyJhbGciOi..."}, "responseTime": 230, "extra": {"env": "prod", "host": "10.0.0.1", "jvm": {"heap": 512, "gc": 5}}}这里class、method、extra.jvm.gc这些字段对业务分析基本没用,requestBody.password和requestBody.token是必须摘掉的敏感字段。配置如下:
filebeat.inputs: - type: filestream id: order-json-log enabled: true paths: - /data/logs/order/*.log parsers: - ndjson: keys_under_root: true overwrite_keys: true add_error_key: true ignore_decoding_error: true processors: - drop_fields: fields: - class - method - requestBody.password - requestBody.token - extra.jvm - extra.host ignore_missing: true关键点在于字段路径的写法。requestBody.password这种带点的写法表示嵌套路径,前提是这一段在文件里确实是一个嵌套对象。如果你的 JSON 本身在requestBody里用的是字符串而不是嵌套对象,那字段路径得写整串才能匹配。
删除敏感字段这件事,我建议不止是删,最好在源头做替换。比如 token 可以用redact或者脚本处理器把内容替换成***,因为有时候业务排查还是需要知道“有没有这个字段”,只是不需要看到真实值。更严格的做法是日志侧直接不打印,但采集端做一次脱敏是性价比最高的双保险。顺便一提,这类敏感字段治理,建议在 index template 层面同时禁用动态添加新字段,防一手将来某些服务日志加了新字段又带出敏感数据。
3.2 场景二:把嵌套JSON扁平成点号键
扁平化是最容易踩坑的场景,因为很多人以为 Filebeat 会自动把嵌套 JSON 拍平,其实默认不会。Filebeat 解析完 JSON 之后,结构是什么样就是什么样,嵌套对象在 ES 里还是嵌套对象。
先看一个典型日志:
{"timestamp": "2024-01-15T10:00:00Z", "data": {"user": {"name": "张三", "age": 30}, "order": {"id": "ORD001", "amount": 99.9}}}如果你只做了 keys_under_root 解析,那么 ES 里的字段是data.user.name、data.user.age、data.order.id、data.order.amount。在 Kibana 里看就是一棵三层树,写查询的时候要写完整路径,做聚合也要展开多层,非常不方便。
要拍平,用flatten处理器:
filebeat.inputs: - type: filestream id: user-order-json-log enabled: true paths: - /data/logs/user/*.log parsers: - ndjson: keys_under_root: true processors: - flatten: field: data separator: "_" ignore_missing: true - drop_fields: fields: - data ignore_missing: true运行之后,字段变成data_user_name、data_user_age、data_order_id、data_order_amount。原来的data这个整体对象被删掉,避免在 ES 里既能看到一层data对象又能看到展开后的平铺字段,出现两份数据。
这里有个自己的偏好问题:separator用下划线_还是点.?官方文档里的例子常用_,因为用点作为分隔符会让 Kibana 把data.user.name又自动识别成嵌套结构,等于白扁平化了。所以除非你有特殊需求,否则一律推荐下划线或者其他不会引起歧义的字符。
flatten 只能处理对象类型字段。如果data本身不是 object 而是数组,flatten不会去遍历数组里的每个元素。这个要注意,很多人试图用 flatten 去拍平数组对象数组,结果发现毫无反应。数组场景要用后面的split。
如果你的 Filebeat 版本比较老、没有 flatten,还有一个土办法:用decode_json_fields把嵌套 JSON 解析出来,然后用一串rename把每个嵌套字段名改成扁平命名。缺点是日志字段结构一变就要跟着改配置,只适合字段稳定的场景。
3.3 场景三:把数组JSON拆成单条事件
数组拆分是我在生产环境见过收益最明显的操作。典型场景:一个批量接口一次处理几十个订单,日志里打印了一个大数组:
{"timestamp": "2024-01-15T10:00:00Z", "traceId": "trace-001", "userId": 10001, "orders": [{"orderId": "A001", "amount": 10.5}, {"orderId": "A002", "amount": 20.0}]}如果原样入库,ES 里是一条文档,里面orders是数组。你想查“某个金额超过 15 的订单明细”,ES 查询虽然也能做,但聚合出来的粒度是整条日志而不是单个订单。要真正按订单明细分析,必须在采集端把数组拆开。
filebeat.inputs: - type: filestream id: batch-order-json-log enabled: true paths: - /data/logs/batch/*.log parsers: - ndjson: keys_under_root: true processors: - split: field: orders target: order keep_parent: true - drop_fields: fields: - orders ignore_missing: true这里field是你要拆的数组字段,target是拆分后每个元素放在哪个字段里,keep_parent表示拆分后是否保留父对象。我遇到最多的问题就是keep_parent设置不对导致外层字段丢失。
需要注意keep_parent: true时,父对象orders还会留在事件里,所以后面要手动drop_fields把orders删掉,否则新的order字段和原来的orders数组并存,数据重复。这个顺序不能反过来:先 split 再 drop,因为 split 依赖orders数组。
拆分之后,每条新事件包含traceId、userId这些公共字段,再加上单个order字段。这样你在 Kibana 里就能按order.orderId做精确过滤,也能按order.amount做聚合,同时用traceId把同一批次的明细再关联回去。整个链路的分析粒度就打通了。
还有一种“拆分”需求比较少见:日志文件里一行拼接了多个 JSON 对象,比如{"a":1}{"b":2},这种 Filebeat 没有现成处理器能干净地拆开,一般要么在源头改打印逻辑,要么用script处理器自己写循环。我不建议硬用处理器去解这种脏数据,优先推动业务侧改输出才是正解。
4. 排障实录:我踩过的那些坑
4.1 字段一直删除不掉,问题出在哪
有段时间同事反馈,drop_fields配了fields: ["trace_id", "request_body"],但 ES 里还能看到这两个字段。排查下来原因有两个。
第一个是字段路径写错。日志里的字段可能是request_body下还有一层对象,比如request_body.password,你只删了request_body,但 keep 了它的子字段——不,这里其实是另一种情况:ES 里看到的字段名可能不是你以为的路径。比如原始 JSON 是{"requestBody": {"password": "x"}},你在配置里写的是requestBody.password,如果 Filebeat 解析时因为 keys_under_root 没开导致所有字段都在json桶里,那真实路径其实是json.requestBody.password,你写的路径自然匹配不上。
第二个是删除顺序问题。如果删字段的 processor 排在 decode 或 flatten 的前面,字段还没生成,删了个寂寞。我把 processors 的执行顺序又验证了一遍,确定要把drop_fields放在所有结构化处理之后。
还有一个隐藏很深的点:有些字段是@metadata里的,或者由 Filebeat 的 module 自动加的。这类字段在输出到 ES 的时候可能被重新装配,drop_fields不一定管用。这个要看你所用的 module 具体实现,不能一概而论。
4.2 明明启用了json解析,字段还是原样字符串
这个坑我刚开始也踩过。配置了json.keys_under_root: true,也看到日志事件里有message字段,里面是一串看起来像 JSON 的字符串,但怎么都不展开成对象。
原因通常是做不到:你启用的整行 JSON 解析,针对的是“这一行的整体内容就是 JSON”,而你现在拿到的是“这一行是普通文本,只有 message 这个字段的值是 JSON 字符串”。这两件事不同,keys_under_root管不到 message 子字段里的内容。
而decode_json_fields里的max_depth也会导致只解析第一层。如果 message 字段里是一个嵌套了两层以上的对象,而max_depth保持默认的 1,那么第二层及以下的 JSON 字段仍然是字符串。排查方法很简单,在 Kibana 里看字段类型,如果出现了大量keyword类型但肉眼看着像对象的数据,基本都是max_depth不够。
还有一种少见但真实的情况:JSON 字符串里带转义字符,比如{\"user\": \"张三\"}里的反斜杠没有正确转义,Filebeat 解析的时候直接失败。这种时候先把add_error_key: true打开,看看error.message里是不是明确写着非法 JSON。
4.3 数组拆分后父级信息丢了
split的keep_parent默认行为在不同版本里可能有差异,我实际碰到的现象是:拆分之后,子事件里只剩数组元素,外层公共字段不在了。原因就是我前面讲的,keep_parent没有设成 true,或者设置的顺序被后面的某个处理器误删了。
另外推荐拆完之后立刻验证事件结构,用filebeat test output配合-d调试日志,直接看处理器输出。我在测试环境会先在processors后面加一个drop_event配合条件做分流,只把测试日志输出到控制台,确认字段没问题再放开。
还有个容易忽略的细节:如果数组是空的,split处理空数组时会怎样?这里要看具体版本行为,稳妥的做法是配一个条件判断,用when.has_fields或者when.equals包裹 split 处理器,空数组就不做拆分,避免产生空事件污染数据。
4.4 扁平化后字段名过长导致的mapping冲突
flatten之后生成的新字段名是拼接出来的,很容易超长。ES 默认字段名没有硬性长度限制,但超过 500 字节的字段名在部分版本会有问题;更重要的是,字段名拼接后可能以数字开头、包含特殊字符、或者两个不同的原始路径拼接出同一个新字段名。
比如{"a_b": 1, "a": {"b": 2}},如果 separator 用_,两个字段都可能变成a_b,后者覆盖前者,数据丢失。这种问题不会报错,但排查起来极其痛苦。我在生产环境被这个坑过一次,最终方案是:扁平化之前先用rename把第一层的重名风险字段改掉,或者干脆在源头确保 JSON key 不包含分隔符。
还有一点:扁平化之后,ES 的 dynamic mapping 可能自动推断出错误类型。比如原本是数字的data_user_age,如果某条日志里这个字段是字符串,mapping 冲突会导致整条索引批量报错。处理办法是在 ES 索引模板里给这些字段预先定义好类型。
4.5 高吞吐下的processor性能取舍
Filebeat 处理器是纯 CPU 操作,每个事件过一遍 processor 都会增加延迟和 CPU 消耗。我压测过一个写得很夸张的配置:一条日志带 5 个 decode_json_fields、3 个 script、每字段都走正则,每秒 2 万条日志时,Filebeat 的 CPU 直接飙到 80% 以上,采集开始积压。
性能问题要这样取舍。尽量用内置处理器,避免写 script,因为 script 要加载 JS 引擎,开销比内置处理器大一个量级。能用drop_fields删字段就绝不写正则去匹配。decode_json_fields的max_depth能小就小,嵌套深的字段如果确实不需要,直接在源头删除更划算。
还有一个容易被忽视的点:processor 处理完再输出,事件体已经变小了,网络传输和 ES 索引压力都会降低。所以一个合理的 processor 配置虽然增加了采集机 CPU 负担,但整体收益大概率是正的。只要别配置成“处理比不处理还重”就行。建议压测时关注三个指标:Filebeat CPU、采集延迟、事件输出字节数,三个一起看才能判断配置是否合理。
5. 生产环境的一次架构总结与个人经验
5.1 Filebeat处理 vs Elasticsearch Ingest Pipeline 怎么选
这个选择题几乎每个用 ES 的团队都会遇到。我给出自己的实践分工。
Filebeat 适合做硬性治理:删敏感字段、删无用字段、解析 JSON、拆分数组。这些操作无论日志发给谁、下游怎么用,都必须做,而且越早做越好。它还能显著减少进 ES 的数据量,是在源头省钱。
ES Ingest Pipeline 适合做弹性加工:Grok 正则解析、GeoIP 地理信息、通过 enrichment processor 关联业务数据。这类操作依赖完整上下文,而且可能需要频繁调整规则,放在 Ingest Pipeline 里改起来不用重新部署采集端。
实际上,Filebeat 处理器和 Ingest Pipeline 是可以共存的:Filebeat 先把大块的 JSON 拆好、删干净,Ingest Pipeline 再做细粒度的富化。我的习惯是:凡是“不做就会出安全问题”的删字段操作,必须放在 Filebeat;凡是“做了更好但可以后面再做”的解析,放到 Ingest Pipeline。
5.2 适合直接抄作业的几条经验结论
最后整理几条思考后可以直接用的经验,都是我实际项目里反复验证过的。
每台采集机上 Filebeat 的 processors 尽量控制在 5 个以内。超过 5 个,排查问题的难度会指数上升,而且大部分复杂逻辑可以下沉到 ES 侧。一个模块只处理一种日志格式。不同服务的日志结构差别很大时,建议拆成多个 input 和对应的 processors,不要试图用一个配置兼容所有格式,那样只会导致谁都不敢动配置。
测试配置一定要用filebeat test config和filebeat test output这两条命令。前者验证语法,后者可以真实拉一条日志看输出结果,看字段有没有删干净、扁平化有没有生效、拆分后结构对不对。我每次改完配置文件,都会在测试环境抓一条真实日志过一遍再上生产。
敏感字段治理要配合索引模板一起做。即使采集端已经把password字段删了,ES 索引模板里也要设置dynamic: false或者把敏感字段名加入ignore列表,防止将来某些日志漏删后又被动态 mapping 收进去。双层保险才算闭环。
我个人踩过很多次坑之后的体会是:Filebeat 处理 JSON 日志的核心原则就是“在源头把结构定死”。日志打印那一刻的结构,决定了后面所有分析的上限。采集端删字段、扁平化、拆分,本质都是在补偿源头设计的不足。所以如果条件允许,优先推动业务侧规范日志格式,把该拆开的拆开、该去掉的去去掉,采集端配置就能做得极简。而在这个基础上,上面的这些 Filebeat 技巧就是最后一道兜底,在格式已经乱掉的情况下,依然能帮你把数据救回来。