第二章:日志格式——标准日志格式解析与自定义日志格式
日志格式这东西,说白了就是日志的「长相」。
我见过不少团队,日志写得乱七八糟,出了故障根本没法查。你想想看,如果每行日志的格式都不一样,你怎么用脚本去分析?怎么快速定位问题?
所以,先把日志格式定死,这是日志分析的第一步。我个人习惯,在项目启动的第一天就把日志格式规范定下来,后面能省80%的排查时间。
2.1 标准日志格式的核心四要素
一个标准的日志行,通常包含四个核心部分。我把它叫做「日志四件套」:
- 时间戳:记录事件发生的精确时刻
- 线程:标识是哪个线程在执行
- 类名:指出是哪个类打印的日志
- 消息:具体的日志内容
嗯,这里要注意,这四个要素缺一不可。我在项目中遇到过,有人为了省事只打印了消息,结果出问题后根本不知道是哪个线程、哪个类出的错,排查起来像大海捞针。
2.1.1 时间戳
时间戳是日志的灵魂。没有时间戳的日志,基本等于废纸。
我个人建议,时间戳一定要包含日期和时分秒毫秒。为什么?因为很多故障发生在毫秒级别。比如两个请求同时到达,没有毫秒你就分不清先后顺序。
常见的格式有:
2025-01-15 14:30:25.123
2025-01-15T14:30:25.123+0800
1610692225123 (Unix时间戳,毫秒级)
2.1.2 线程
线程信息能告诉你「谁」在执行这段代码。特别是在高并发场景下,多个线程同时跑,没有线程名你根本分不清哪个请求对应哪个日志。
标准格式示例:
[http-nio-8080-exec-10]
[main]
[Thread-3]
你看,线程名里通常包含了线程池的名称和编号。这在实际排查中非常有用。比如你看到 http-nio-8080-exec-10,就知道这是Tomcat处理HTTP请求的线程。
2.1.3 类名
类名告诉你日志是从哪个类打印出来的。这就像地图上的坐标,能帮你快速定位到代码位置。
通常有两种写法:
- 全限定类名:
com.example.service.UserService - 简写类名:
UserService
我个人推荐使用全限定类名。为什么?因为项目大了以后,同名类很多。比如多个模块都有 ConfigService,只看简写你根本分不清是哪个。
2.1.4 消息
消息就是日志要表达的具体内容。这部分最容易被写烂。
好的消息应该包含:
- 操作描述:比如「用户登录成功」
- 关键参数:比如用户ID、订单号
- 执行结果:成功还是失败,耗时多少
举个例子:
❌ 不好的写法:登录成功
✅ 好的写法:用户[userId=12345]登录成功,耗时[15ms]
2.2 标准日志格式示例
把上面四个要素组合起来,就是一个完整的标准日志行:
2025-01-15 14:30:25.123 [http-nio-8080-exec-10] INFO com.example.service.UserService - 用户[userId=12345]登录成功,耗时[15ms]
这个格式看起来简单,但实际项目中,不同框架的默认格式略有不同。我整理了一个对比表:
| 框架 | 默认格式 | 特点 |
|---|---|---|
| Logback | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n |
最常用,性能好 |
| Log4j2 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n |
支持异步日志 |
| Java Util Logging | %1$tb %1$td, %1$tY %1$tl:%1$tM:%1$tS %1$Tp %2$s%n%4$s: %5$s%n |
JDK自带,功能弱 |
2.3 自定义日志格式
标准格式能满足大部分场景,但有些时候你需要定制。比如:
- 公司有统一的日志规范
- 需要加入额外的上下文信息(如TraceId)
- 日志分析工具对格式有特殊要求
这时候就需要自定义日志格式了。我以Logback为例,展示如何自定义:
2.3.1 在logback.xml中配置
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>
</configuration>
你看,我在标准格式里加了一个 [%X{traceId}],这就是MDC(Mapped Diagnostic Context)的用法。它能让你在日志里打印请求链路的唯一标识。
2.3.2 常用占位符一览
| 占位符 | 含义 | 示例输出 |
|---|---|---|
%d |
日期时间 | 2025-01-15 14:30:25.123 |
%thread |
线程名 | http-nio-8080-exec-10 |
%level |
日志级别 | INFO, ERROR |
%logger |
类名 | com.example.service.UserService |
%msg |
日志消息 | 用户登录成功 |
%X{key} |
MDC上下文 | traceId-abc123 |
%n |
换行符 | (换行) |
2.4 实战中的格式选择建议
说了这么多,到底该用哪种格式?我根据不同的场景,给几个建议:
- 开发环境:用彩色输出,方便肉眼查看。可以加类名和行号
- 测试环境:用标准格式,方便日志收集
- 生产环境:用精简格式,减少IO开销。去掉不必要的字段
嗯,这里有个小技巧。生产环境的日志,我建议把类名缩短。比如 c.e.s.UserService 代替 com.example.service.UserService。这样每行日志能省几十个字节,一天下来能省不少磁盘空间。
2.5 常见问题与避坑
最后,分享几个我踩过的坑:
- 时间格式不一致:不同模块用了不同的时间格式,导致日志分析工具解析失败。统一用
yyyy-MM-dd HH:mm:ss.SSS - 忘记加线程名:排查并发问题时,没有线程名根本没法分析。一定要加上
- 类名太长:全限定类名太长会影响性能。可以用
%logger{36}截断 - 消息里带换行:日志消息里如果包含换行符,会破坏日志结构。记得用
replace处理一下
我曾经因为日志格式没统一,导致一个线上故障排查了整整两天。后来我强制团队使用统一的日志模板,再也没出过类似问题。
记住,日志格式不是小事。它决定了你出故障时,是花10分钟解决问题,还是花10小时大海捞针。