第二章:日志格式——标准日志格式解析与自定义日志格式

日志格式这东西,说白了就是日志的「长相」。

我见过不少团队,日志写得乱七八糟,出了故障根本没法查。你想想看,如果每行日志的格式都不一样,你怎么用脚本去分析?怎么快速定位问题?

所以,先把日志格式定死,这是日志分析的第一步。我个人习惯,在项目启动的第一天就把日志格式规范定下来,后面能省80%的排查时间。

2.1 标准日志格式的核心四要素

一个标准的日志行,通常包含四个核心部分。我把它叫做「日志四件套」:

  • 时间戳:记录事件发生的精确时刻
  • 线程:标识是哪个线程在执行
  • 类名:指出是哪个类打印的日志
  • 消息:具体的日志内容

嗯,这里要注意,这四个要素缺一不可。我在项目中遇到过,有人为了省事只打印了消息,结果出问题后根本不知道是哪个线程、哪个类出的错,排查起来像大海捞针。

2.1.1 时间戳

时间戳是日志的灵魂。没有时间戳的日志,基本等于废纸。

我个人建议,时间戳一定要包含日期和时分秒毫秒。为什么?因为很多故障发生在毫秒级别。比如两个请求同时到达,没有毫秒你就分不清先后顺序。

常见的格式有:

2025-01-15 14:30:25.123
2025-01-15T14:30:25.123+0800
1610692225123  (Unix时间戳,毫秒级)
我的经验: 尽量使用带时区的时间格式。我曾经因为时区问题,排查一个跨时区的系统故障,日志时间差了8个小时,白白浪费了半天时间。

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。这样每行日志能省几十个字节,一天下来能省不少磁盘空间。

我的习惯: 我会在logback.xml里配置多个appender,不同环境用不同的格式。开发环境用console输出,生产环境用文件输出。这样既方便开发,又保证生产性能。

2.5 常见问题与避坑

最后,分享几个我踩过的坑:

  • 时间格式不一致:不同模块用了不同的时间格式,导致日志分析工具解析失败。统一用 yyyy-MM-dd HH:mm:ss.SSS
  • 忘记加线程名:排查并发问题时,没有线程名根本没法分析。一定要加上
  • 类名太长:全限定类名太长会影响性能。可以用 %logger{36} 截断
  • 消息里带换行:日志消息里如果包含换行符,会破坏日志结构。记得用 replace 处理一下

我曾经因为日志格式没统一,导致一个线上故障排查了整整两天。后来我强制团队使用统一的日志模板,再也没出过类似问题。

记住,日志格式不是小事。它决定了你出故障时,是花10分钟解决问题,还是花10小时大海捞针。