本文目录导读:

Java 日志级别的规整(结构设计)是构建可观测性系统的基础,一个好的日志级别结构不是简单的“INFO, WARN, ERROR”,而是需要根据你的日志框架、业务场景以及监控告警需求进行分层设计。
以下从标准级别定义、实际使用规范、框架差异和团队规约四个维度来拆解。
标准日志级别金字塔(最核心)
任何规整的日志系统都应该遵循清晰的“严重程度”分层,从低到高通常为:
| 级别 | 英文 | 使用场景 | 生产环境建议 |
|---|---|---|---|
| TRACE | 跟踪 | 代码执行的详细路径、SQL参数、循环内打印 | 关闭,仅开发调试 |
| DEBUG | 调试 | 开发/测试环境排查问题,记录变量值、函数进入/离开 | 关闭,可动态开启 |
| INFO | 信息 | 重要业务节点:服务启动、请求入口/出口、核心状态变化 | 开启,记录关键节点 |
| WARN | 警告 | 潜在问题:业务异常(如库存不足)、即将过期的配置、降级、资源接近阈值 | 开启,需关注但无需立即处理 |
| ERROR | 错误 | 已发生的异常:无法处理的故障(数据库连不上)、系统崩溃、预期外的Exception | 开启,触发警报 |
| FATAL | 致命 | 系统即将或已经不可用:配置严重错误、内存溢出、无法恢复的故障 | 开启,直接触发电话告警 |
核心原则:
- INFO 不要刷屏:每个请求只打印 1-3 行,而不是将循环体内的内容打印出来(那是 DEBUG 的事)。
- ERROR 是留给代码 Bug 和系统异常的:不要把业务校验失败(如“用户名已存在”)直接打成 ERROR(应打 WARN)。
- FATAL 现在很少用:现代框架(如 Log4j2, Logback)通常无 FATAL,直接用 ERROR + 自定义级别或通过告警条件区分。
框架层面的级别规整
不同 Java 日志框架的级别支持不同,需要统一适配:
| 框架 | 支持的级别(部分列表) | 备注 |
|---|---|---|
| Log4j2 | OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL | 支持自定义级别(如 AUDIT,需慎用) |
| Logback | OFF, ERROR, WARN, INFO, DEBUG, TRACE, ALL | 无 FATAL,用 ERROR 替代 |
| SLF4J | ERROR, WARN, INFO, DEBUG, TRACE | 日志门面,只暴露最通用的5个级别 |
| JDK Logging | SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST | 语法混乱,建议用 SLF4J 统一 |
规整建议:
- 统一使用 SLF4J 作为门面,避免直接依赖 Log4j 或 Logback 的 API。
- 如果你必须使用 FATAL,可以加到 Logback 的配置中(通过复制 ERROR 级 logger 并改名),但不推荐,更现代的做法是:在 ERROR 级日志中包含
"fatal"关键词,由告警系统(如 Prometheus + AlertManager)做高优先级的规则匹配。
实际业务中的“规整”套路(反模式与最佳实践)
什么情况下打什么级别?
| 场景 | 应该用 | 解释 |
|---|---|---|
| 用户登录成功 | INFO | 正常业务流程节点 |
| 用户登录失败(密码错) | WARN | 业务异常预期内,不是系统错误 |
| 数据库连接池满(重试成功) | WARN | 系统有降级机制,但需注意 |
| 数据库连接池满(重试失败) | ERROR | 系统功能受损 |
| 第三方 API 调用超时(降级返回默认值) | WARN | 有降级方案 |
| 第三方 API 调用超时(导致请求失败) | ERROR | 影响了可用性 |
| NullPointerException(catch 后打印) | ERROR | 代码 Bug |
| 主键冲突 | WARN | 通常属于业务逻辑问题 |
| 初始化加载配置失败 | FATAL/ERROR | 系统无法启动 |
永远不要做这几件事(破坏结构):
- catch Exception 后只打 ERROR,却不传异常对象:
// 错误:丢失堆栈信息 logger.error("操作失败"); // 正确:传入异常对象,打印完整堆栈 logger.error("操作失败", exception); - 使用
logger.error记录业务错误码:错误码应单独作为 MDC(Mapped Diagnostic Context)变量,而不是放在 message 里混淆。 - 在循环内打 log:尤其是 INFO 级别,会直接打爆磁盘。
- 规整做法:在循环外打汇总日志,或者抽样打印(每1000条打一次)。
工具:MDC 结构化日志(让级别规整更容易)
为了让“规整”能落地到数据结构中,推荐使用 MDC + JSON 格式,日志级别只是其中一个字段,这样便于查询和聚合。
推荐日志格式(结构化 JSON):
{
"@timestamp": "2025-04-11T10:15:30.123Z",
"level": "ERROR",
"logger": "com.your.service.OrderService",
"thread": "http-nio-8080-exec-3",
"traceId": "abc123def456",
"userId": "u9999",
"message": "订单创建失败",
"stack_trace": "java.sql.SQLException: ...",
"duration_ms": 350,
"custom_context": {
"orderId": "ORD12345",
"sku": "SKU001"
}
}
规整效果:
- 通过
level字段可以快速筛选。 - 通过
traceId可以将一次请求的所有日志串联。 - 通过
custom_context直接携带业务参数(而不需要拼在 message 里)。
实现方式:
import org.slf4j.MDC;
// 在请求入口(Filter/Interceptor)设置
MDC.put("traceId", generateTraceId());
MDC.put("userId", request.getUserId());
// ...
// 日志框架会自动将 MDC 中的变量输出到 JSON 格式中
logger.warn("库存不足", orderId); // 此时日志会自动带上 traceId 和 userId
// 在请求结束后清除
MDC.clear();
团队规约:写在代码规范中
要真正“规整”,需要在团队 Git Hook 或 CI 中强制检查:
- 禁用
System.out.println:通过 Checkstyle 或 Sonar 规则拦截。 - 异常必须传第二个参数:
// 坏 logger.error(e.getMessage()); // 好 logger.error("业务描述", e); - 业务异常用 WARN,系统异常用 ERROR:强制在 Code Review 中审核。
- 禁止在循环体内部打 INFO/ERROR:除非有明确的抽样策略(如
if (i % 1000 == 0))。
一个规整的日志级别结构表
| 维度 | 规范 | 工具/方法 |
|---|---|---|
| 级别选择 | TRACE/D(本地),INFO(业务),WARN(容错),ERROR(故障) | SLF4J 门面 |
| 输出格式 | 统一 JSON 结构化,包含 level, traceId, message |
Logback + Jackson |
| 上下文 | 使用 MDC 传递请求级变量 | MDC.put / MDC.clear |
| 异常处理 | logger.error("msg", exception) |
禁止 e.getMessage() |
| 性能 | 循环内不打 INFO,重度日志加 if (logger.isDebugEnabled()) |
条件判断 |
最终结论:规整的日志结构 = SLF4J 门面 + 严格级别语义 + JSON 结构化 + MDC 跟踪 + 异常堆栈完整。