日志这事,写多了烦人,写少了出事。线上出问题排查半天,发现日志要么没打,要么打了一堆没用的。
今天聊点实际的,总结一下怎么写好日志。
日志级别用对了吗
java
// ERROR - 程序出错了,需要人工处理
logger.error("数据库连接失败,第{}次重试", retryCount, ex);
// WARN - 有问题,但程序还能跑
logger.warn("当前库存为0,用户:{}", userId);
// INFO - 正常业务流程的关键节点
logger.info("用户下单成功,订单号:{}", orderId);
// DEBUG - 开发调试用,生产环境默认不输出
logger.debug("收到消息,topic:{}, partition:{}", topic, partition);
级别越高越重要。线上出问题,先看 ERROR,再看 WARN。
什么时候该打日志
1. 系统启动和停止
java
// 应用启动
logger.info("=== 应用启动,端口:{} ===", port);
logger.info("数据库连接池初始化完成,最大连接:{}", maxPoolSize);
// 应用关闭
logger.info("=== 收到关闭信号,开始优雅停机 ===");
2. 业务关键节点
java
// 好的例子 - 记录关键流程
logger.info("用户登录成功,用户ID:{},来源:{}", userId, source);
logger.info("订单支付成功,订单号:{},金额:{}", orderId, amount);
logger.info("异步任务开始执行,任务ID:{},类型:{}", taskId, taskType);
3. 外部调用
java
// 调用外部接口
logger.info("调用支付接口,订单号:{},金额:{}", orderId, amount);
long start = System.currentTimeMillis();
try {
PaymentResponse response = paymentClient.pay(request);
logger.info("支付接口调用成功,耗时:{}ms", System.currentTimeMillis() - start);
} catch (Exception e) {
logger.error("支付接口调用失败,订单号:{},耗时:{}ms,错误:{}",
orderId, System.currentTimeMillis() - start, e.getMessage());
}
4. 异常捕获
java
// 不好的写法
try {
doSomething();
} catch (Exception e) {
log.error("出错了"); // 打了等于没打
}
// 好的写法
try {
doSomething();
} catch (BusinessException e) {
logger.error("业务异常,业务编码:{},错误信息:{}", e.getCode(), e.getMessage());
} catch (Exception e) {
logger.error("系统异常,操作:{},用户ID:{},异常类型:{}",
operation, userId, e.getClass().getName(), e);
}
什么时候不该打日志
1. 循环内的日志
java
// 不好的写法
for (OrderItem item : orderItems) {
logger.info("处理订单项:{}", item.getId()); // 1000个订单就是1000条日志
}
2. 不要打印敏感信息
java
// 不好的写法
logger.info("用户登录,用户名:{},密码:{}", username, password); // 密码都打出来了
// 好的写法
logger.info("用户登录,用户名:{},来源:{}", username, source);
3. 不要打印无意义的调试信息
java
// 不好的写法
logger.info("进入方法A");
logger.info("开始查询数据库");
logger.info("查询结束");
// 这些日志没有任何价值,线上全是噪音
日志内容要规范
1. 带上关键上下文
java
// 不好的写法
logger.error("处理失败");
// 好的写法
logger.error("订单处理失败,订单号:{},用户ID:{},原因:{}", orderId, userId, reason);
2. 统一日志格式
# 推荐格式
[时间] [级别] [线程] [类名] [traceId] - 消息
# 例子
2024-01-15 10:23:45.123 INFO [http-nio-8080-exec-1] [OrderService] [abc123] 用户下单成功,订单号:ORDER001
3. 使用占位符而不是字符串拼接
java
// 不好的写法
logger.info("用户" + userId + "下单成功,金额" + amount);
// 好的写法
logger.info("用户{}下单成功,金额{}", userId, amount);
// 原因:字符串拼接即使不打印也会执行,浪费性能
traceId 打通全链路
微服务架构下,一个请求经过多个服务,排查问题需要 traceId 串联:
java
// 拦截器:请求进来时生成 traceId
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
TraceContext.setTraceId(traceId);
MDC.put("traceId", traceId);
return true;
}
}
// 配置文件
logback.xml
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%thread] [%logger{36}] [%X{traceId}] - %msg%n</pattern>
// 日志输出
2024-01-15 10:23:45.123 INFO [http-nio-8080-exec-1] [OrderService] [abc123] 用户下单成功
2024-01-15 10:23:45.125 INFO [http-nio-8080-exec-1] [PaymentService] [abc123] 开始调用支付
几个实用技巧
1. 参数太长的处理
java
// 订单备注可能有几百个字符
String remark = order.getRemark();
logger.info("用户下单成功,订单号:{},备注:{}",
orderId, remark != null && remark.length() > 50 ? remark.substring(0, 50) + "..." : remark);
2. 集合内容的打印
java
List<Long> ids = Arrays.asList(1L, 2L, 3L);
// 不要直接打印集合,可能很大
logger.info("订单IDs:{}", ids); // 集合太大会撑爆日志
// 打印长度和前几个
logger.info("订单IDs数量:{},前10个:{}",
ids.size(), ids.stream().limit(10).collect(Collectors.toList()));
3. 异步日志防丢失
java
// 重要业务日志用异步方式避免阻塞
logger.info("关键业务操作,用户:{},操作:{},结果:{}", userId, operation, result);
// logback 配置异步 appender
<appender name="ASYNC_INFO" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="INFO_FILE"/>
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
日志归档和清理
xml
<!-- logback 配置日志归档 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
总结
| 场景 | 建议 |
|---|---|
| 系统启动/停止 | INFO,记录关键配置 |
| 业务关键节点 | INFO,带齐上下文参数 |
| 外部调用 | INFO 开始,WARN/ERROR 结果 |
| 异常捕获 | ERROR,记录堆栈和关键参数 |
| 循环内 | 不要打,或打摘要 |
| 敏感信息 | 不要打,脱敏处理 |
好的日志:能帮你在凌晨3点快速定位问题,而不是让你在几千行日志里猜。