#日志级别与最佳实践
日志级别的混乱是团队中常见的「技术债」:日志满天飞,关键信息被淹没;或者日志太少,排查时找不到需要的信息。
问题往往不是不知道有哪些日志级别,而是不知道什么时候该用哪个级别。本文给出一个清晰的决策框架。
#日志级别定义
#四级日志体系
| 级别 | 英文 | 中文 | 定义 | 产生频率 |
|---|---|---|---|---|
| ERROR | Error | 错误 | 需要立即处理的错误 | 低 |
| WARN | Warning | 警告 | 潜在问题,不需要立即处理 | 中 |
| INFO | Information | 信息 | 重要的业务里程碑 | 高 |
| DEBUG | Debug | 调试 | 详细的开发调试信息 | 极高 |
#错误级别判断标准
使用以下三个问题判断一条日志应该是什么级别:
1. 用户是否受到影响?
└── 是 → ERROR
└── 否 → 继续
2. 需要人工干预吗?
└── 是 → ERROR
└── 否 → WARN
3. 是否是异常情况(但预期可能发生)?
└── 是 → WARN
└── 否 → INFO 或 DEBUG#各级别使用场景
#ERROR:错误需要立即处理
ERROR 意味着系统出现了需要人工干预的问题。用户可能已经受到影响,或即将受到影响。
ERROR
// ✅ 正确:外部依赖完全失败,用户请求无法完成
log.error("Payment service unavailable after 3 retries: orderId={}, error={}",
orderId, e.getMessage(), e);
// ✅ 正确:数据一致性被破坏
log.error("Order state inconsistent: expected=PAID, actual=PENDING, orderId={}",
orderId);
// ✅ 正确:无法继续处理的致命错误
log.error("Database connection pool exhausted after timeout", e);
// ❌ 错误:一个请求的参数错误是用户问题,不是 ERROR
log.error("Invalid parameter: userId={}", userId); // 应该是 WARN#WARN:警告潜在问题
WARN 意味着系统发现了一个潜在问题,不需要立即处理,但值得关注。可能是配置异常、资源接近上限、或业务逻辑的非预期情况。
WARN
// ✅ 正确:资源接近上限,需要关注但不是故障
log.warn("Connection pool utilization high: used={}/total={}, threshold=80%",
used, total);
// ✅ 正确:配置使用默认值,可能不是预期行为
log.warn("Cache TTL not configured, using default 300 seconds");
// ✅ 正确:业务降级,核心功能仍可用但性能下降
log.warn("Redis unavailable, falling back to local cache. Performance may degrade.");
// ✅ 正确:重试成功前的失败尝试(记录最后一次)
log.warn("Payment retry succeeded after 2 attempts: orderId={}, totalAttempts=3",
orderId);
// ✅ 正确:业务规则校验失败(用户输入问题)
log.warn("Order cancelled by user: orderId={}, reason=USER_REQUESTED", orderId);
// ❌ 错误:框架的内部 WARN 应该被抑制或降级
log.warn("Slow query detected: duration=100ms, sql={}", sql); // 框架日志#INFO:业务里程碑
INFO 是正常业务流程中的关键节点,用于审计、追踪和了解系统运行状态。
INFO
// ✅ 正确:请求入口和出口
log.info("Request received: method={}, uri={}, traceId={}",
request.getMethod(), request.getRequestURI(), traceId);
log.info("Request completed: status={}, duration={}ms, traceId={}",
response.getStatus(), duration, traceId);
// ✅ 正确:关键业务操作
log.info("Order placed: orderId={}, amount={}, paymentMethod={}",
order.getId(), order.getAmount(), order.getPaymentMethod());
log.info("Payment completed: transactionId={}, amount={}, status={}",
transaction.getId(), transaction.getAmount(), transaction.getStatus());
// ✅ 正确:系统状态变化
log.info("Application started: version={}, port={}", version, port);
log.info("Configuration reloaded: keys={}", changedKeys);
// ✅ 正确:定时任务执行
log.info("Daily report generation completed: records={}, duration={}s",
recordCount, durationSeconds);
// ❌ 错误:循环内的日志(会产生海量日志)
for (Order order : orders) {
log.info("Processing order: {}", order.getId()); // 应该用 DEBUG
}
// ❌ 错误:每个方法都记录入口
public void doSomething() {
log.info("Entering doSomething"); // 噪音太大
// ...
}#DEBUG:开发调试
DEBUG 是开发调试和详细分析用的信息,生产环境通常关闭。
DEBUG
// ✅ 正确:方法入参和出参(仅开发环境)
log.debug("Entering method: calculateOrderTotal, params: orderId={}, items={}",
orderId, items);
log.debug("Method result: total={}", total);
// ✅ 正确:详细的执行路径(用于定位 bug)
log.debug("Cache miss for key: {}", cacheKey);
log.debug("Cache hit for key: {}, value: {}", cacheKey, value);
// ✅ 正确:循环中的详细状态
log.debug("Processing item: index={}, itemId={}, status={}",
index, item.getId(), item.getStatus());
// ✅ 正确:SQL 语句和参数
log.debug("Executing query: sql={}, params={}", sql, params);#日志内容设计
#好日志的标准
包含足够上下文:
// ❌ 错误:上下文不足,不知道是哪个订单、哪个用户
log.info("Order cancelled");
// ✅ 正确:包含足够定位问题的信息
log.info("Order cancelled: orderId={}, userId={}, reason={}, traceId={}",
orderId, userId, reason, traceId);结构化数据优于字符串拼接:
// ❌ 错误:字符串拼接,解析困难
log.info("User " + userId + " placed order " + orderId);
// ✅ 正确:参数化日志,结构清晰
log.info("User order placed: userId={}, orderId={}, amount={}",
userId, orderId, amount);异常日志要包含堆栈:
// ❌ 错误:只记录消息,丢失堆栈信息
log.error("Payment failed: " + e.getMessage());
// ✅ 正确:传入异常对象,保留完整堆栈
log.error("Payment failed for orderId={}", orderId, e);#环境差异配置
#Logback 环境配置
logback-spring.xml
<!-- 开发环境:全部开启 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 生产环境:INFO 及以上 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- 框架日志降级 -->
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="org.apache.catalina" level="WARN"/>
</springProfile>
<!-- 测试环境:DEBUG -->
<springProfile name="test">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>#常见反模式
#反模式一:日志级别滥用
// ❌ 所有日志都是 INFO,ERROR 和 WARN 被淹没
log.info("Entering method");
log.info("Exiting method");
log.info("If condition reached");
// ✅ 正确:区分重要事件和普通路径
log.info("Order created: orderId={}", orderId);
log.debug("Validation passed: fields={}", fields);#反模式二:日志成为性能瓶颈
// ❌ 字符串拼接在日志级别检查之前执行
log.info("Processing user: " + buildUserSummary(user)); // 拼接总是执行
// ✅ 正确:参数化,拼接只在 INFO 开启时执行
log.info("Processing user: userId={}", user.getId());
// ✅ 正确:先检查级别
if (log.isInfoEnabled()) {
log.info("Processing user: {}", buildUserSummary(user));
}#反模式三:日志中的敏感信息
// ❌ 错误:记录密码、Token、身份证等敏感信息
log.info("User login: userId={}, password={}", userId, password);
// ✅ 正确:脱敏处理
log.info("User login: userId={}, hasPassword=true", userId);#日志级别升级机制
对于持续出现但不需要立即处理的 ERROR,可以考虑自动升级:
public class EscalatingLogger {
private final Logger logger;
private final Map<String, AtomicInteger> errorCount = new ConcurrentHashMap<>();
public void error(String msg, Object... params) {
String key = msg;
int count = errorCount.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
if (count == 1) {
logger.error(msg, params);
} else if (count == 10) {
// 10 次重复错误后升级为告警(通过 metrics)
metrics.increment("error.escalated", tag("message", msg));
logger.warn("Error repeated {} times: {}", count, msg);
}
// 超过阈值后降低频率
if (count > 100 && count % 100 == 0) {
logger.error("Error still repeating: count={}, {}", count, msg);
}
}
}#质量判断标准
读完本节后,你应该能够回答:
- 使用三个问题判断日志级别的标准是什么?请分别用 ERROR、WARN、INFO、DEBUG 各举一个具体例子。
- 为什么说「字符串拼接在日志级别检查之前执行」是一个性能反模式?应该如何正确处理?
- 为什么框架日志(如 Spring、Hibernate)在生产环境应该降级为 WARN?这些框架的 DEBUG 日志会产生多少噪音?
- 在 ERROR 日志中,传入异常对象和只传入异常消息有什么区别?为什么堆栈信息如此重要?
- 如何设计一个日志级别升级机制,防止同一错误反复刷屏?