日志最佳实践:记录什么、怎么记录
日志写得好,排查问题时事半功倍;写得烂,日志再多也是废纸。
日志框架选择
Java 日志框架经过多年混战,目前主流组合是 SLF4J + Logback:
| 框架 | 说明 | 现状 |
|---|---|---|
| Log4j | 早期霸主 | 已停止维护,不推荐 |
| Log4j2 | Apache 重写版,性能好 | 仍在维护 |
| SLF4J | 日志门面(API 层) | 事实标准 |
| Logback | Log4j 作者重写,Spring Boot 默认 | 主流选择 |
推荐组合:SLF4J(API)+ Logback(实现)
使用占位符
java
// ❌ 字符串拼接:即使 INFO 级别不输出,拼接也已执行
log.info("用户: " + userName + " 订单: " + orderId);
// ✅ 占位符:INFO 级别不输出时,拼接不会执行
log.info("用户: {} 订单: {}", userName, orderId);SLF4J 的 {} 占位符是延迟求值的,只有日志级别匹配时才会拼接字符串。
不要记录敏感信息
java
// ❌ 禁止:敏感信息日志
log.info("用户登录成功,用户名: {} 密码: {}", username, password);
log.info("Token: {}", token);
// ✅ 脱敏处理
log.info("用户登录成功,用户名: {}", username);
log.info("Token: {}", maskToken(token));
// 脱敏方法
private String maskToken(String token) {
if (token == null || token.length() < 8) return "***";
return token.substring(0, 4) + "****" + token.substring(token.length() - 4);
}业务日志规范
结构化日志
java
// ❌ 非结构化日志:grep 难以精确查找
log.info("用户注册成功: userId=10001, phone=138****1234");
// ✅ 结构化日志:Key-Value 格式
log.info("用户注册成功 | userId={} | phone={}", 10001, "138****1234");
// ✅ 或者用 MDC 传递上下文
MDC.put("userId", "10001");
MDC.put("traceId", "abc123");
log.info("用户注册成功");
// 输出自动带上 traceId 和 userId统一日志格式
xml
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>%X{traceId} 会输出 MDC 中存储的 traceId,方便在 ELK 中按请求追踪。
日志级别选择
| 级别 | 含义 | 生产环境 |
|---|---|---|
| ERROR | 错误,已影响功能 | 记录 |
| WARN | 警告,可能有问题 | 记录 |
| INFO | 正常运行流程 | 记录 |
| DEBUG | 调试信息 | 通常关闭 |
| TRACE | 最详细追踪 | 通常关闭 |
总结
- 用 SLF4J + Logback:标准组合,配置简洁
- 占位符
{}:避免不必要的字符串拼接 - 不记录敏感信息:密码、token 都要脱敏
- 结构化日志:用 Key-Value 格式,方便检索
- 带上上下文:traceId、userId 等字段对排查问题至关重要
日志的第一读者不是人,是 ELK。写日志时要想着怎么让搜索更方便。
