4、结构化日志:JSON格式日志、日志字段标准化、ELK体系简介

说实话,我见过太多团队在排查线上问题时,对着满屏的纯文本日志抓耳挠腮。那种日志,说白了就是一堆用空格或竖线隔开的字符串。你想想看,要从中提取一个用户ID或者请求耗时,得靠正则表达式去硬抠,效率低不说,还容易出错。

我个人习惯,从项目第一天就推行结构化日志。这玩意儿,说白了就是把日志从「给人看」变成「给机器看」。而JSON格式,就是目前最通用的结构化日志标准。

4.1 为什么是JSON?

JSON的好处,我总结下来就三点:

  • 自描述:每个字段都有名字,看到 "level": "ERROR" 就知道是错误级别,不用猜。
  • 机器友好:几乎所有编程语言都有现成的JSON解析库,解析效率极高。
  • 易于扩展:加一个新字段,不会影响老版本的解析逻辑。

我在项目中遇到过最典型的反面教材:一个老系统用 | 分隔符拼日志,结果业务方在消息内容里也用了 |,导致整条日志解析错位。排查了整整两天,最后发现是转义没处理好。换成JSON,这种问题根本不会发生。

4.2 JSON格式日志实战

我们先看一个对比。这是传统的纯文本日志:

2024-01-15 10:30:45.678 ERROR [http-nio-8080-exec-3] c.e.s.UserService - 用户登录失败 | userId=12345 | reason=密码错误 | cost=234ms

这是结构化后的JSON日志:

{
  "timestamp": "2024-01-15T10:30:45.678+08:00",
  "level": "ERROR",
  "thread": "http-nio-8080-exec-3",
  "logger": "com.example.service.UserService",
  "message": "用户登录失败",
  "userId": 12345,
  "reason": "密码错误",
  "costMs": 234,
  "serviceName": "user-service",
  "traceId": "a1b2c3d4e5f6"
}

你看,哪个更清晰?一目了然。而且 costMs 是数字类型,可以直接做聚合计算,比如统计平均耗时。

在Python里,我一般这样输出结构化日志:

import json
import logging

class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_record = {
            "timestamp": self.formatTime(record, self.datefmt),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "function": record.funcName,
            "line": record.lineno
        }
        # 合并 extra 字段
        if hasattr(record, 'extra_fields'):
            log_record.update(record.extra_fields)
        return json.dumps(log_record, ensure_ascii=False)

# 使用示例
logger = logging.getLogger("user_service")
logger.setLevel(logging.INFO)

handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)

# 输出带业务字段的日志
logger.info("用户登录成功", extra={"extra_fields": {"userId": 12345, "costMs": 45}})
我的小技巧:在 extra 里传业务字段时,建议统一用一个字典包裹(比如 extra_fields),避免跟标准LogRecord属性冲突。我曾经因为这个踩过坑,字段名跟 message 重了,日志直接报错。

4.3 日志字段标准化

光有JSON格式还不够。如果每个团队各写各的字段名,那跟纯文本也没啥区别。比如A团队用 user_id,B团队用 userId,C团队用 uid——你想想看,在ELK里做关联查询时得多痛苦。

我建议,团队内部必须制定一套日志字段规范。下面是我常用的标准字段清单:

字段名 类型 说明 是否必填
timestamp string (ISO8601) 日志产生时间,带时区 是
level string 日志级别:DEBUG/INFO/WARN/ERROR/FATAL 是
logger string 记录器名称,通常是类名或模块名 是
message string 人类可读的日志消息 是
traceId string 分布式链路追踪ID 强烈建议
spanId string 当前调用跨度ID 强烈建议
serviceName string 微服务名称 是
host string 主机名或IP 建议
costMs number 耗时,单位毫秒 按需
userId number/string 用户标识 按需
exception object 异常信息,包含type/message/stackTrace 异常时必填
避坑指南:我曾经遇到过一个团队,把 timestamp 存成了时间戳数字(如 1705297845),结果在Kibana里做时间范围查询时,还得额外写转换脚本。统一用ISO8601字符串,兼容性最好,Kibana也能自动识别。

4.4 ELK体系简介

日志结构化了,字段也标准化了,接下来就是怎么收集、存储和分析了。这时候,ELK体系就登场了。

ELK是三个开源项目的首字母缩写:

  • Elasticsearch:分布式搜索和分析引擎,负责存储和检索日志。
  • Logstash:服务器端数据处理管道,负责采集、转换和发送日志。
  • Kibana:可视化平台,负责展示日志数据和构建仪表盘。

后来Elastic公司又加入了 Beats 系列轻量级采集器,所以现在更常说的是 Elastic Stack(ELK + Beats)。

整个工作流程,我画了张图,你看一眼就明白了:

ELK 日志处理流程 应用服务 应用服务 应用服务 Filebeat 轻量采集器 Logstash 解析 / 过滤 转换 / 增强 Elasticsearch 存储 + 检索 Kibana 可视化 流程说明: 1. 应用服务输出JSON格式日志到本地文件 2. Filebeat 监控日志文件,实时读取并发送到 Logstash 3. Logstash 对日志进行解析、过滤、字段标准化后写入 Elasticsearch 4. 运维/开发人员在 Kibana 中搜索、分析、可视化日志数据

这个流程里,结构化日志是基础。如果日志本身不是JSON格式,Logstash就得用grok正则去解析,性能差不说,还容易出错。我见过一个极端案例:某团队日志里有个字段值特别长,grok正则直接超时,导致整个Logstash管道阻塞。换成JSON后,问题瞬间解决。

4.5 在ELK中查询结构化日志

日志进了Elasticsearch,查询就变得非常灵活。比如,我想查某个用户最近一小时的登录失败记录:

# Kibana 查询语句
serviceName: "user-service" AND level: "ERROR" AND reason: "密码错误" AND userId: 12345

再比如,我想统计所有服务的平均响应时间:

# Elasticsearch 聚合查询
{
  "aggs": {
    "avg_cost": {
      "avg": {
        "field": "costMs"
      }
    },
    "by_service": {
      "terms": {
        "field": "serviceName.keyword"
      },
      "aggs": {
        "avg_cost": {
          "avg": {
            "field": "costMs"
          }
        }
      }
    }
  }
}

你看,有了结构化字段,这些查询写起来就跟写SQL一样自然。要是纯文本日志,你得先写正则把 costMs 抠出来,再转成数字,才能做聚合。这中间每一步都可能出错。

核心要点:结构化日志 + 字段标准化 + ELK体系,这三者缺一不可。没有结构化日志,ELK的威力发挥不出来;没有字段标准化,日志数据就是一团乱麻;没有ELK,结构化日志的价值也大打折扣。

嗯,这一章的内容就到这里。记住,从今天开始,让你的日志「结构化」起来。这是你走向高效运维的第一步。