日志记录的基本原理:日志级别、格式规范、写入流程

日志这东西,平时大家可能觉得就是“println”的升级版。但真要排查线上事故,你才发现,日志写得好不好,直接决定了你能不能准时下班。

我个人习惯把日志看作系统的“黑匣子”。飞机失事要查黑匣子,系统出问题就得翻日志。但黑匣子也不是什么数据都录,它得按级别、按格式、按规范来。这一章我们就聊聊这三个核心点。

1. 日志级别——不是什么都要记

日志级别,说白了就是给日志分个优先级。从最啰嗦到最紧急,一般分这几档:

级别 含义 我常用的场景
TRACE 跟踪 开发时临时跑一次,线上基本不开
DEBUG 调试 本地排查逻辑分支、变量状态
INFO 信息 记录关键节点,比如服务启动、请求入口
WARN 警告 参数异常、降级、非预期但能处理的场景
ERROR 错误 异常捕获、业务中断、需要人介入
FATAL 致命 进程崩溃、严重资源耗尽
我的小建议: 生产环境默认只开 WARN 及以上,INFO 就够多了。但排查特定问题时,记得动态调低级别,别一直开 DEBUG 跑,磁盘很快会报警。

你想想看,线上每秒几千个请求,如果每个请求都打 TRACE,日志量能暴涨几十倍。那时候你连查日志本身都成了问题。我经历过一个项目,就因为误把日志级别设成了 DEBUG,一天内打满了 200GB 的磁盘空间,系统直接挂了。

所以级别选择是日志设计的第一步。不同场景选不同级别,这是一个成熟工程师的基本功。

2. 格式规范——机器和人都不瞎

日志格式,不只是给人看的,更重要的,是给后续的日志收集、分析、回溯系统看的。你想想看,如果没有统一格式,你在上万条日志里找一条请求的全链路,得靠眼神检索吗?

我建议日志至少包含以下字段:

  • 时间戳:精确到毫秒,最好带时区
  • 日志级别:PREFIX 标识,比如 [ERROR]
  • 线程名:便于追踪异步调用
  • 模块名:知道是哪个子系统打出来的
  • 代码位置:类名、方法名、行号
  • 消息体:描述问题或状态的核心信息
  • 异常栈:捕获异常时带上全栈信息
  • 上下文字段:比如 traceId、userId、orderId

一个典型的格式化日志长这样:

2025-04-15 14:23:45.678 [ERROR] [http-nio-8080-exec-3] [OrderService] [orderBiz:123] 订单状态转移失败,订单ID=ORD202504150001,当前状态=PENDING,期望状态=PAID → 异常:java.lang.IllegalStateException: 订单状态不匹配
  at com.demo.service.OrderService.updateStatus(OrderService.java:156)
  at com.demo.controller.OrderController.payOrder(OrderController.java:42)
避坑指南: 我曾经见过一个系统,日志里只写了“参数错误”,连是什么参数、什么场景都没说。查那个 Bug 花了整整两天。所以,日志消息体一定要包含足够的关键上下文,别怕啰嗦。

日志格式的通用结构,我建议遵循下面这个模板:

字段 示例 是否必填
时间戳 2025-04-15 14:23:45.678 是
级别 [ERROR] 是
线程 [http-nio-8080-exec-3] 推荐
模块 [OrderService] 推荐
请求标识 [traceId=abc123] 非常推荐
消息体 订单状态转移失败... 是
异常栈 at ... 异常时必填

在日志缺失场景下,我们恰恰需要利用这些字段来重建现场。比如 traceId 可以让多个模块的日志串联起来;时间戳可以确定前后顺序;模块名能定位故障扩散路径。所以,平时把格式规范好,就是在为未来的回溯铺路。

核心思维: 统一格式 = 可解读的日志。乱格式 = 噪音。

3. 写入流程——日志是怎么落到磁盘的

很多同学以为日志就是“print 之后立刻写文件”。其实不是。日志的写入有一套完整的流水线,理解它,你才能解释为什么日志可能丢、可能延迟、可能会影响性能。

日志写入流程大致分几步:

  1. 事件发生:业务代码调用了 logger.info() / logger.error()
  2. 生成日志记录:框架创建 LogRecord 对象,包含时间、级别、消息、异常等
  3. 格式序列化:格式器将 LogRecord 转换成字符串(比如上面的标准格式)
  4. 日志过滤:过滤器判断这条日志是否要输出(级别、关键字等)
  5. 写入缓冲区:日志先进入内存缓冲区,避免每条日志都立即触发磁盘 I/O
  6. 异步刷入:后台线程定期或按容量将缓冲区刷新到磁盘文件
  7. 输出目标:文件、控制台、网络(日志中心)、数据库等等
警告: 异步写入虽然性能好,但带来的副作用就是:如果系统突然崩溃(比如 OOM、kill -9),缓冲区里还没来得及刷盘的日志就会丢失。这就是所谓的“日志缺失”的一种常见原因。你一定要做好权衡。

这七步,我画了一张图,帮你看清整个过程:

日志写入流程 ① 事件发生 ② 生成日志记录 ③ 格式序列化 ④ 日志过滤 ⑤ 写入缓冲区 ⑥ 异步刷入 ⑦ 输出目标 关键说明 缓冲区异步刷入提升性能,但也导致崩溃时可能丢失最新日志;格式规范直接影响后续回溯效率

这张图里,你看从第5步到第7步,中间有个缓冲区。这就是为什么日志“看起来”慢了半拍,也是为什么系统突然断电会丢日志。理解了这个流程,你就能在配置日志框架时,合理设置缓冲区大小、刷盘策略、异步线程数。

举个例子,我以前调过一个实时交易系统的日志。为了不丢单,我们把异步刷盘改成了同步写入,每次日志都马上落盘。结果压测时性能下降了 40%。后来我们做了一层妥协:ERROR 级别同步写,INFO 及以下异步写。既保证了关键日志不丢,也不拖累正常请求。

关于日志缺失的启示: 日志缺失通常就发生在以下几个节点——缓冲区未刷盘(崩溃丢失)、格式不规范导致无法解析(信息缺失)、级别过滤导致关键日志被忽略(人为缺失)。理解了日志写入流程,你就知道从哪下手防治。

小结一下

日志级别、格式规范、写入流程,这三者不是孤立的。级别决定你记什么,格式决定你怎么记,写入流程决定记完是否安全。在日志缺失场景下,我们需要反过来用:缺失了哪些级别?缺失位置的格式是否完整?是否有缓冲区未刷盘导致的丢失?