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)。
整个工作流程,我画了张图,你看一眼就明白了:
这个流程里,结构化日志是基础。如果日志本身不是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 抠出来,再转成数字,才能做聚合。这中间每一步都可能出错。
嗯,这一章的内容就到这里。记住,从今天开始,让你的日志「结构化」起来。这是你走向高效运维的第一步。