日志采集与格式化:从杂乱到有序

日志这东西,说实话,在没出事之前没人当回事。我早年做过一个数据泄密溯源的项目,客户说“日志全着呢”,结果一查——全是原始文本,没有时间戳标准化,没有来源标识,根本没法关联分析。你想想看,监控审计的第一步是什么?不是存,而是收和洗。

这章我就把日志采集和格式化这两个点掰开揉碎讲。咱们先看常见的几种日志格式,再说说怎么用工具把日志弄进来。

常见日志格式:Syslog、JSON、CEF

干安全监控这些年,我遇到过不下十种日志格式。但真正能打、用得广的,就下面三种。

格式结构特征典型场景我遇到过的坑
Syslog纯文本行,RFC 3164/5424,有facility、severity、时间戳、消息网络设备(防火墙、路由器)、Linux系统日志时间戳格式不统一,有的用本地时间没有时区,跨区域分析直接翻车
JSON键值对嵌套结构,UTF-8编码Web应用、API网关、SaaS服务字段名大小写混乱,source_ip和SourceIP并存,解析脚本要写一堆兼容逻辑
CEF (Common Event Format)以“CEF:Version|DeviceVendor|DeviceProduct|...”开头的竖线分隔扩展格式SIEM对接、商业安全产品(如McAfee、Check Point)自定义扩展字段的key-value部分没有统一转义规则,有时候|符号出现在字段里导致解析错位
个人建议:如果你们团队要从零搭建日志中心,优先选JSON。因为JSON自解释性好,字段扩展灵活,比Syslog的定长结构好维护,比CEF那套竖线分隔更容易调试。

聊聊Syslog的细节

Syslog是最古老的,也是设备兼容性最强的。但它的致命伤是缺乏结构化。比如下面这段:

<14>Jan  5 10:15:30 192.168.1.1 sshd[12345]: Failed password for root from 10.0.0.1 port 22

“Jan 5”这个时间格式,在不同locale下可能翻译成“Jan 5”还是“1月 5日”,没有时区信息。我曾经在跨机房的日志聚合中,因为Syslog默认用了UTC+8的本地时间,结果另一台设备用的UTC,时间差了8小时,排查了整整两天。

所以你现在看到很多新一代的Syslog实现(RFC 5424)加了结构化数据段,但老旧设备依然用RFC 3164。我的做法是:在采集端统一做一次时间戳归一化,用Filebeat的timestamp处理插件,或者Fluentd的record_modifier。

JSON——最省心的选择

JSON灵活,但也不是没有坑。举个例子:

{ "event": { "type": "login", "source_ip": "10.0.0.1", "user": "admin", "status": "success" } }

看着很漂亮对吧?但如果在不同系统里,“source_ip”有时写成“src_ip”、“client_ip”,或者“status”用“success”/“1”/“true”三种布尔表示,你的解析逻辑立马变成一团乱麻。我建议在日志生成源头就定一个schema规范,比如DSPM里常用的ECS (Elastic Common Schema),字段名、类型、值枚举都定死,后面处理才省心。

CEF——SIEM的老朋友

CEF本质上是扩充后的竖线分隔格式,前半部分固定:

CEF:0|Security|ThreatManager|1.0|100|Login failure|10|src=10.0.0.1 dst=192.168.1.1 duser=admin

竖线后面的key-value部分用空格分隔,key=value。看起来简单,但扩展字段没有统一的长度或类型约束。我记得有一次对接某家EDR产品,它的CEF日志里把恶意软件MD5写成了“md5=abcdef1234567890”,但其他厂商可能写成“fileHash=abc...”,导致规则引擎的映射表得有几十个别名。

警告:不要轻易使用CEF的自定义扩展字段来处理复杂结构。如果字段值里包含空格或=号,必须转义,但很多设备不按规范来。JSON在这一点上优势明显。

采集工具:Filebeat vs Fluentd

格式搞清楚了,怎么把日志从各个机器上拉过来?我在生产环境里主要用过两套:Filebeat(Elastic生态)和Fluentd(Cloud Native Computing Foundation项目)。下面这张图是我个人总结的采集管道结构——

日志采集与格式化流程 应用/设备 原始日志 网络设备 Syslog/UDP 云服务API JSON/HTTP 采集工具 Filebeat / Fluentd (轻量、插件化) JSON 结构化/ECS Syslog RFC 5424 CEF 竖线分隔 日志存储/分析 采集工具通常内置格式化插件 无需额外转换层

Filebeat——Elastic家族的轻量前锋

Filebeat的特点就是轻、稳、跟ELK深度绑定。我曾经在一个320台服务器的集群上部署,每个节点只占不到12MB内存,CPU几乎没波动。它内部的处理管道是这样的:

  • 输入:支持log文件、tcp/udp、docker日志等。我最常用的是filestream输入,比起旧版的log输入,它解决了我头疼多年的日志轮转跟随问题。
  • 处理:通过processors模块做字段提取、时间戳转换、IP地理位置、drop字段等。我曾经用它把混乱的Syslog时间戳统一成ISO8601,一行配置搞定:
processors:
  - timestamp:
      field: syslog_timestamp
      layouts:
        - 'Jan  2 15:04:05'
        - '2006-01-02T15:04:05Z07:00'
      timezone: UTC

说实话,Filebeat的配置语法是YAML,比Fluentd的DSL容易上手。但它的短板很明显:数据转换能力相对弱,复杂的分支逻辑、多数据源关联,它做不了。遇到那种需要把一条日志拆成多条事件的场景,我就得上Logstash或Fluentd。

Fluentd——云原生环境下的瑞士军刀

Fluentd的设计哲学是“一切都是插件”。它由Ruby编写,内置了超过1000个插件,从输入到输出再到Filter,几乎可以在管道里做任何事。

我在处理Kubernetes容器日志时特别喜欢用它。Pod日志默认输出到stdout,Fluentd通过tail input追读,然后加上Pod的元数据(namespace、pod name、container name),再格式化输出给Elasticsearch或S3。

看一个典型配置片段:

<source>
  @type tail
  path /var/log/containers/*.log
  pos_file /var/log/fluentd-containers.log.pos
  tag kubernetes.*
  format json
  time_key time
</source>

<filter kubernetes.**>
  @type kubernetes_metadata
</filter>

<match kubernetes.**>
  @type elasticsearch
  host logstash.example.com
  port 9200
  flush_interval 5s
</match>
关键区别:Fluentd的管道是多路输入输出,可以在单个进程里消费多个源;而Filebeat更倾向于单进程单输入。当你要从文件、TCP、UDP、Windows Event Log同时采集时,Fluentd的灵活性体现得更明显。

我如何选型?一张表说清

维度FilebeatFluentd
资源消耗极低(~12MB/实例)中等(~50MB/实例,Ruby VM)
数据格式支持内置对Syslog、JSON、CEF有简单解析器;复杂格式需写脚本通过插件支持几乎所有格式;自定义filter很灵活
与Elastic Stack集成原生,无缝发送ES需额外配置ES输出插件,但也能用
Kubernetes支持通过DaemonSet部署,内置自动发现pod元数据使用Fluentd DaemonSet + filter插件,功能更强(支持Pod标签富化)
错误处理重试 + 死信队列,但配置项不如Fluentd细支持retry、timeout、secondary输出等精细化策略

我的建议:如果你团队主要用ES做分析,环境以传统服务器为主,选Filebeat,省心。如果你要对接多种存储(ES、S3、Kafka、MongoDB等),或者日志变换逻辑复杂,选Fluentd。我在一个混合云项目里甚至同时用了两者——边缘服务器用Filebeat发到Kafka,Fluentd订阅Kafka做ETL后再写ES。

避坑指南:格式化阶段的三条血泪教训

教训一:不要信任日志中的IP字段。我曾经收到一台Switch的Syslog,src_ip字段里出现了IPv6地址但未加方括号,导致JSON解析失败。解决方案:在采集阶段统一用正则或格式化插件做规范化,例如对IP字段应用CIDR转换,或者强制转成字符串再写入。

教训二:一定要处理多行日志。Java异常堆栈、SQL慢查询、多行JSON对象,如果每行单独发送,原始事件会被肢解。Filebeat的multiline配置可以按正则合并:

multiline.type: pattern
multiline.pattern: '^(\d{4}-\d{2}-\d{2}|%{TIMESTAMP_ISO8601})'
multiline.negate: true
multiline.match: after

Fluentd则用@type multiline插件。我习惯先做一条样本日志,用在线正则工具调试好再上线。

教训三:字段类型一致性。同一个字段在不同日志源里可能是字符串“123”或者整型123,到了Elasticsearch里mapping就乱了。我要求在Filebeat processor里加一个convert:

processors:
  - convert:
      fields:
        - {from: "duration", to: "integer"}

嗯,以上这些,都是真金白银的线上经验。等你真正搭建起DSPM日志采集层,就会发现——格式搞定了,后面的事就顺了。