日志记录的基本原理:日志级别、格式规范、写入流程
日志这东西,平时大家可能觉得就是“println”的升级版。但真要排查线上事故,你才发现,日志写得好不好,直接决定了你能不能准时下班。
我个人习惯把日志看作系统的“黑匣子”。飞机失事要查黑匣子,系统出问题就得翻日志。但黑匣子也不是什么数据都录,它得按级别、按格式、按规范来。这一章我们就聊聊这三个核心点。
1. 日志级别——不是什么都要记
日志级别,说白了就是给日志分个优先级。从最啰嗦到最紧急,一般分这几档:
| 级别 | 含义 | 我常用的场景 |
|---|---|---|
| TRACE | 跟踪 | 开发时临时跑一次,线上基本不开 |
| DEBUG | 调试 | 本地排查逻辑分支、变量状态 |
| INFO | 信息 | 记录关键节点,比如服务启动、请求入口 |
| WARN | 警告 | 参数异常、降级、非预期但能处理的场景 |
| ERROR | 错误 | 异常捕获、业务中断、需要人介入 |
| FATAL | 致命 | 进程崩溃、严重资源耗尽 |
你想想看,线上每秒几千个请求,如果每个请求都打 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)
日志格式的通用结构,我建议遵循下面这个模板:
| 字段 | 示例 | 是否必填 |
|---|---|---|
| 时间戳 | 2025-04-15 14:23:45.678 | 是 |
| 级别 | [ERROR] | 是 |
| 线程 | [http-nio-8080-exec-3] | 推荐 |
| 模块 | [OrderService] | 推荐 |
| 请求标识 | [traceId=abc123] | 非常推荐 |
| 消息体 | 订单状态转移失败... | 是 |
| 异常栈 | at ... | 异常时必填 |
在日志缺失场景下,我们恰恰需要利用这些字段来重建现场。比如 traceId 可以让多个模块的日志串联起来;时间戳可以确定前后顺序;模块名能定位故障扩散路径。所以,平时把格式规范好,就是在为未来的回溯铺路。
3. 写入流程——日志是怎么落到磁盘的
很多同学以为日志就是“print 之后立刻写文件”。其实不是。日志的写入有一套完整的流水线,理解它,你才能解释为什么日志可能丢、可能延迟、可能会影响性能。
日志写入流程大致分几步:
- 事件发生:业务代码调用了 logger.info() / logger.error()
- 生成日志记录:框架创建 LogRecord 对象,包含时间、级别、消息、异常等
- 格式序列化:格式器将 LogRecord 转换成字符串(比如上面的标准格式)
- 日志过滤:过滤器判断这条日志是否要输出(级别、关键字等)
- 写入缓冲区:日志先进入内存缓冲区,避免每条日志都立即触发磁盘 I/O
- 异步刷入:后台线程定期或按容量将缓冲区刷新到磁盘文件
- 输出目标:文件、控制台、网络(日志中心)、数据库等等
这七步,我画了一张图,帮你看清整个过程:
这张图里,你看从第5步到第7步,中间有个缓冲区。这就是为什么日志“看起来”慢了半拍,也是为什么系统突然断电会丢日志。理解了这个流程,你就能在配置日志框架时,合理设置缓冲区大小、刷盘策略、异步线程数。
举个例子,我以前调过一个实时交易系统的日志。为了不丢单,我们把异步刷盘改成了同步写入,每次日志都马上落盘。结果压测时性能下降了 40%。后来我们做了一层妥协:ERROR 级别同步写,INFO 及以下异步写。既保证了关键日志不丢,也不拖累正常请求。
小结一下
日志级别、格式规范、写入流程,这三者不是孤立的。级别决定你记什么,格式决定你怎么记,写入流程决定记完是否安全。在日志缺失场景下,我们需要反过来用:缺失了哪些级别?缺失位置的格式是否完整?是否有缓冲区未刷盘导致的丢失?