背景

在多部门、多技术栈并存的企业环境中,日志收集与分析是保障系统稳定运行的核心能力之一。然而,不同开发团队采用各异的日志打印方式,导致日志数据结构混乱,严重影响后续的收集、存储、检索与告警效率。

比如我们大部门就有多套日志格式,不同小部门有不同的格式,导致我们运维任务和成本大幅度增加。
典型问题场景包括:

开发团队A使用Log4j,格式为[%d] %p %c - %m%n
团队B采用Logback,格式为%date %level [%thread] %logger - %msg%n
微服务C输出JSON格式日志但缺少统一字段规范

统一日志格式规范与 Filebeat+Logstash 实践落地_堆栈

本文将介绍我们如何建立统一的日志格式规范,并基于 Filebeat + Logstash 实现多环境(宿主机/Kubernetes)下的高效日志采集、解析与存储。

为什么要统一日志格式?

遇到的问题:

  • 各部门日志格式五花八门,结构不一
  • 多行异常堆栈无法完整还原,频繁切段
  • traceId/请求上下文缺失,无法链路追踪
  • 结构化字段难以提取,告警系统误报频繁

统一格式的好处:

  • 日志标准化后,便于多系统集中分析
  • traceId 实现服务链路跟踪(可接入 Skywalking/Jaeger)
  • 多行异常可合并为单条日志事件
  • 方便在 Elasticsearch 中建立字段索引,用于筛选、聚合与报警

日志格式统一规范

在设计日志标准时,我们遵循以下关键原则:

机器可读性:便于采集工具自动解析
人类可读性:保留足够上下文,便于工程师直接阅读
完整性与高效性的平衡:包含必要字段但不过度冗余
可扩展性:支持未来增加新字段而不破坏兼容性
行业兼容性:符合主流日志框架的最佳实践

经过多轮评审,最终确定的标准日志格式为:

%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%X{traceId:-null}] [%thread] [%logger{36}] [%L] - %msg%n%wEx
  • 1.

样例日志:

2025-04-09 13:36:39.947 [INFO ] [null] [main] [o.s.c.s.PostProcessorRegistrationDelegate$BeanPostProcessorChecker] [327] - Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$d712abb8] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
  • 1.

Grok语法

%{TIMESTAMP_ISO8601:date} \[\s{0,2}(?<loglevel>[^\]]+)\] \[\s{0,2}(?<traceId>[^\]]+)\] \[(?<thread>[^\]]+)\] \[(?<logger>[^\]]+)\] \[(?<codeline>\d+)\] - %{GREEDYDATA:msg}
  • 1.

统一日志格式规范与 Filebeat+Logstash 实践落地_JSON_02

这个标准格式包含七大关键要素:

时间戳:精确到毫秒的ISO8601格式,确保跨时区协调
日志级别:统一5字符宽度对齐(ERROR/WARN/INFO/DEBUG)
追踪标识:分布式系统必备的traceId,缺省为null
线程信息:帮助理解异步处理流程
类名:缩写为36字符保持简洁
行号:精准定位日志产生位置
消息体:包含可选的异常堆栈信息

字段占位符

对应字段

说明

必要性

示例

%d{yyyy-MM-dd HH:mm:ss.SSS}

时间戳

精确到毫秒的ISO8601格式

必要

2023-08-15 14:30:45.123

%-5level

日志级别

右对齐,5字符宽度

必要

INFO / ERROR

%X{traceId:-null}

请求链路ID

分布式追踪标识

重要

3f5e8a2b1c9d4f7e

%thread

线程名

执行线程标识

必要

http-nio-8080-exec-1

%logger{36}

类名

截断到36字符的类全名

必要

com.fjf.service.PaymentService

%L

代码行号

日志调用处的行号

可选

42

%msg

日志内容

实际日志信息

核心

“用户支付成功,金额:100.00”

%wEx

异常堆栈

附带异常时的堆栈信息

条件

java.lang.NullPointerException…

多语言实现示例

Java (Log4j2):

<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%X{traceId:-null}] [%thread] [%-36.36logger{36}] [%.-1L] - %msg%n%wEx"/>
  • 1.

Python:

import logging
FORMAT = '%(asctime)s [%(levelname)-5s] [%(traceId)s] [%(threadName)s] [%(name)-36s] [%(lineno)d] - %(message)s'
  • 1.
  • 2.

日志采集方案设计

混合环境下的采集策略
面对物理机与Kubernetes并存的混合环境,我们设计了分层采集方案:

宿主机 Filebeat 配置
# filebeat.inputs配置
filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /app/logs/app/hx-app.log
    - /app/logs/app/hx-consume.log
    - /fjf_work/logs/fujfu_member/fujfu_member_admin.log
  # 多行日志处理关键配置
  multiline.pattern: ^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}[.,:]0{0,1}\d{3}
  multiline.negate: true
  multiline.match: after
  # 元数据字段
  fields:
    logtype: "hx"

fields_under_root: true
fields:
  ip: 192.18.199.160

output.logstash:
  hosts: ["logstash.server.fjf:5044"]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

技术关键点:

  • multiline.pattern使用正则严格匹配标准格式的时间戳开头
  • negate: true + match: after确保堆栈信息被正确关联到主日志行
  • 通过fields添加主机IP等元信息,便于后续定位问题来源
Kubernetes Pod 内部 Filebeat ConfigMap

filebeat-configmap.yaml 可以注册到configmap中

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /app/logs/app/*.log
  multiline.pattern: ^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}[.,:]0{0,1}\d{3}
  multiline.negate: true
  multiline.match: after
  fields:
    logtype: ${LOGTYPE:app}

fields_under_root: true
fields:
  ip: ${POD_IP}  # 自动注入Pod IP

output.logstash:
  hosts: ["logstash-server.default.svc.cloudcs.fjf:5044"]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

✅ 说明:多行合并通过正则识别日志起始行(时间戳),其余行自动归入上一条日志。

K8s特定优化:

  • 使用环境变量动态注入POD_IP和LOGTYPE
  • 通配符路径匹配适配不同应用的日志文件
  • 通过DaemonSet确保每个节点都有采集器
Logstash 解析规则设计

在 Logstash 中通过 filter 插件完成日志结构化处理,区分标准格式、PowerJob 特例与 JSON 格式:

filter {
  # JSON日志特殊处理
  if [fields][json] == "true" {
    json {
      source => "message"
      remove_field => ["message","agent","tags","ecs"]
    }
  }
  # 标准格式解析
  else {
    grok {
      match => { "message" => [
        "%{TIMESTAMP_ISO8601:date} \[\s{0,2}(?<loglevel>[^\]]+)\] \[\s{0,2}(?<traceId>[^\]]+)\] \[(?<thread>[^\]]+)\] \[(?<logger>[^\]]+)\] \[(?<codeline>\d+)\] - %{GREEDYDATA:msg}",
        # 备用模式匹配历史格式
      ]}
    }
  }
  
  # 添加业务标记
  ruby {
    code => 'event.set("projectname", event.get("host")["name"].split(".")[0])'
  }
  
  # 时间处理
  date {
    match => ["date","ISO8601","yyyy-MM-dd HH:mm:ss.SSS"]
    target => "@timestamp"
    timezone => "Asia/Shanghai"
  }
}
  • 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.

生产案例:

# ===========================
# 1. 输入配置(Beats 输入)
# ===========================
input {
  beats {
    port => 5044                      # 接收来自 Filebeat 的数据
    ssl => false                      # 未启用 SSL(可以视需求启用)
    client_inactivity_timeout => 36000 # 客户端连接空闲超时(秒)
  }
}
# ===========================
# 2. 过滤器(filter)
# ===========================
filter {

  # 如果是 JSON 格式日志(如前端或Node服务打印的 JSON 日志)
  if [fields][json] == "true" {
    json {
      source => "message"             # 指定从 message 字段中解析 JSON
      remove_field => ["message", "agent", "tags", "ecs"] # 清理多余字段
      add_field => {
        "loglevel" => "%{[severity]}" # 从 JSON 中提取 severity 字段作为 loglevel
      }
    }

  # 如果是 PowerJob 的日志格式(特殊格式日志单独处理)
  } else if [fields][logtype] == "powerjob" {
    grok {
      match => {
        "message" => "%{TIMESTAMP_ISO8601:date} \s{0,1}(?<severity>.*?) (?<pid>.*?) --- \[(?<thread>.*?)\] (?<class>.*?) \s*: (?<rest>.*+?)"
      }
      remove_field => ["message", "agent", "tags"]
      add_field => {
        "loglevel" => "%{[severity]}"
      }
    }
    mutate {
      update => {
        "[fields][logtype]" => "logstash" # 为统一索引,将 logtype 设置为 logstash
      }
    }

  # 默认解析逻辑:标准日志格式(适配多种格式)
  } else {
    grok {
      match => { 
        "message" => [
          # 标准推荐格式日志
          "%{TIMESTAMP_ISO8601:date} \[\s{0,2}(?<loglevel>[^\]]+)\] \[\s{0,2}(?<traceId>[^\]]+)\] \[(?<thread>[^\]]+)\] \[(?<logger>[^\]]+)\] \[(?<codeline>\d+)\] - %{GREEDYDATA:msg}",
          
          # 其他兼容格式 (防止有漏网之鱼,加的格式,注意格式越多,处理也耗费CPU)
          "%{TIMESTAMP_ISO8601:date} (?<loglevel>.*?)\s{1,2}\| \[(?<threadname>.*?)\] (?<classname>.*?) \[(?<codeline>.*?)\] \| \[(?<traceid>.*?)\] \| (?<msg>.*+?)",
          "%{TIMESTAMP_ISO8601:date} (?<loglevel>.*?)\s{1,2}\| \[(?<threadname>.*?)\] (?<classname>.*?) \[(?<codeline>.*?)\] \| (?<msg>.*+?)",
          "\[%{TIMESTAMP_ISO8601:date}\] \[(?<loglevel>.*?)\s{0,2}\] \[(?<threadname>.*?)\] (?<classname>.*?) (?<codeline>.*?) - (?<msg>.*+?)",
          "%{TIMESTAMP_ISO8601:date} \[(?<threadname>.*?)\] (?<loglevel>.*?)\s{0,2} (?<classname>.*?) (?<codeline>.*?) - (?<msg>.*+?)"
        ]
      }
      remove_field => ["message", "agent", "tags"] # 删除原始 message 字段等无用字段
    }
  }

  # Ruby 脚本:提取主机名的前缀作为项目名
  ruby {
    code =>'
      arr = event.get("host")["name"].split(".")[0]
      event.set("projectname", arr)
    '
  }

  # 时间字段统一转换为 @timestamp,便于时序检索
  date {
    match => ["date", "ISO8601", "yyyy-MM-dd HH:mm:ss.SSS"]
    target => "@timestamp"
    timezone => "Asia/Shanghai"   # 设置中国时区,避免时间错乱
  }

}

# ===========================
# 3. 输出到 Elasticsearch
# ===========================
output {
  elasticsearch {
    hosts => ["elasticsearch-log.prod.server.fjf:9200"]  # ES 地址
    user => "elastic"
    password => "xxxxxxxxxxxx"
    manage_template => false     # 不自动覆盖 ES 模板(防止冲突)
    index => "%{[fields][logtype]}-prod-%{+YYYY.MM.dd}"  # 动态索引名(按日志类型和日期)
  }
}
  • 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.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.

关键处理阶段:

  • 格式识别路由:区分JSON和文本日志
  • 多模式Grok解析:主模式匹配标准格式,备用模式兼容历史格式
  • 元数据丰富:从主机名提取项目标识
  • 时间标准化:统一转化为ES兼容的时间戳

统一日志格式规范与 Filebeat+Logstash 实践落地_JSON_03

实践效果

引入统一日志系统后取得的可测量改进:

  1. 故障定位时间:从平均2.5小时缩短至30分钟
  2. 日志存储量:通过合理字段设计减少25%存储需求
  3. 日志利用率:结构化日志使90%的日志可被监控系统利用
  4. 异常检测:基于标准字段建立的规则发现率提升40%