前言
日志是程序的"黑匣子",它记录了程序运行时的关键信息。在实际开发中,良好的日志体系能帮助我们快速定位问题、分析系统性能、监控业务状态。然而,很多开发者在编写日志时存在随意性,导致日志要么冗余混乱,要么信息不足。
本文将分享如何编写高质量的日志,从基本原则、日志级别、最佳实践等方面进行详细说明。
一、日志的基本原则
1. 明确目标
在编写日志前,问自己三个问题:
- 这条日志的目的是什么?(调试?监控?审计?)
- 谁会看这条日志?(开发人员?运维人员?业务人员?)
- 看到这条日志后能做什么?
2. 一致性
保持日志格式的统一,包括时间格式、层级缩进、关键字段命名等。
3. 可读性
日志最终是给人看的,要让阅读者能快速理解日志的含义。
二、合理使用日志级别
大多数日志框架都提供以下五个级别,合理选择至关重要:
| 级别 | 使用场景 | 示例 |
|---|---|---|
| ERROR | 严重的错误,需要立即处理 | 数据库连接失败、业务异常导致流程中断 |
| WARN | 潜在的问题,但不影响当前流程 | 配置未设置使用默认值、重试操作 |
| INFO | 重要的业务节点或状态变化 | 服务启动完成、订单创建成功、定时任务开始执行 |
| DEBUG | 开发调试信息,定位问题用 | 方法入参、SQL语句、循环中的关键变量 |
| TRACE | 最详细的调试信息 | 每一步的执行路径、详细的数据流转 |
原则:
- 生产环境通常只开启 INFO 及以上级别
- DEBUG 日志仅在开发和测试环境开启
- 避免将敏感信息(密码、token)打印到日志
三、日志内容的最佳实践
1. 包含必要的上下文信息
java
// 不推荐
log.info("用户登录成功");
// 推荐
log.info("用户登录成功, userId={}, ip={}, loginTime={}",
userId, ipAddress, LocalDateTime.now());
2. 结构化日志
对于复杂系统,建议使用 JSON 格式的结构化日志:
java
// 使用MDC添加额外上下文
MDC.put("traceId", traceId);
MDC.put("userId", userId);
log.info("orderCreated",
new OrderLog(orderId, amount, paymentMethod));
// 输出示例:
// {"timestamp":"2024-01-01 10:00:00","level":"INFO","message":"orderCreated","orderId":"123","amount":99.9}
3. 异常日志要详细
java
try {
// 业务逻辑
} catch (Exception e) {
// 不推荐
log.error("发生错误:" + e.getMessage());
// 推荐
log.error("处理订单失败, orderId={}, errorType={}, detail={}",
orderId, e.getClass().getSimpleName(), e.getMessage(), e);
}
4. 日志要可搜索
java
// 使用固定的关键词前缀
log.info("[PAYMENT_SUCCESS] 支付成功, orderId={}, amount={}", orderId, amount);
log.info("[PAYMENT_FAILED] 支付失败, orderId={}, reason={}", orderId, reason);
这样在日志系统中可以快速搜索 PAYMENT_SUCCESS 来统计支付成功率。
四、性能考虑
1. 避免字符串拼接
java
// 不推荐 - 即使日志级别不输出也会执行字符串拼接
log.debug("用户信息:" + user.toString() + ",时间:" + System.currentTimeMillis());
// 推荐 - 使用占位符,只在需要输出时才执行参数转换
log.debug("用户信息:{},时间:{}", user, System.currentTimeMillis());
2. 使用条件判断
对于复杂的数据组装,应先判断日志级别:
java
if (log.isDebugEnabled()) {
String complexInfo = buildComplexInfo(data);
log.debug("复杂调试信息:{}", complexInfo);
}
3. 控制日志输出频率
对于高频调用的方法,考虑采样或限流:
java
// 每100次输出一次日志
if (count.incrementAndGet() % 100 == 0) {
log.info("处理进度:已处理 {} 条记录", count.get());
}
五、不同场景的日志策略
1. 接口层
java
@PostMapping("/api/order")
public Result createOrder(@RequestBody OrderRequest request) {
log.info("[API_REQUEST] 创建订单, request={}", request);
long startTime = System.currentTimeMillis();
try {
Result result = orderService.create(request);
log.info("[API_RESPONSE] 创建订单完成, cost={}ms, result={}",
System.currentTimeMillis() - startTime, result);
return result;
} catch (Exception e) {
log.error("[API_ERROR] 创建订单异常, cost={}ms, error={}",
System.currentTimeMillis() - startTime, e.getMessage(), e);
throw e;
}
}
2. 业务层
java
@Service
public class OrderService {
@Transactional
public void processOrder(Long orderId) {
log.info("[PROCESS_ORDER_START] 开始处理订单, orderId={}", orderId);
// 关键步骤记录日志
log.info("订单状态变更为处理中, orderId={}, oldStatus={}, newStatus={}",
orderId, oldStatus, OrderStatus.PROCESSING);
// 调用外部服务
try {
paymentService.pay(orderId);
log.info("支付成功, orderId={}", orderId);
} catch (PaymentException e) {
log.error("支付失败, orderId={}, errorCode={}",
orderId, e.getCode(), e);
throw e;
}
log.info("[PROCESS_ORDER_END] 订单处理完成, orderId={}", orderId);
}
}
3. 定时任务
java
@Component
public class ReportTask {
@Scheduled(cron = "0 0 1 * * *")
public void generateDailyReport() {
String taskId = UUID.randomUUID().toString();
MDC.put("taskId", taskId);
log.info("[TASK_START] 开始生成日报");
try {
// 任务执行
log.info("查询数据完成, 共{}条记录", count);
// 输出统计信息
log.info("今日订单统计:总金额={},订单数={}", totalAmount, orderCount);
log.info("[TASK_END] 日报生成完成");
} catch (Exception e) {
log.error("[TASK_ERROR] 日报生成失败", e);
} finally {
MDC.clear();
}
}
}
六、日志框架选型建议
主流日志框架
- SLF4J:日志门面,推荐作为统一的日志API
- Logback:Spring Boot默认,功能强大,性能好
- Log4j2:异步日志性能极佳,适合高并发场景
配置示例(Logback)
xml
<configuration>
<!-- 定义日志格式 -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 文件滚动输出 -->
<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</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 异步输出,提升性能 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<!-- 设置日志级别 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC"/>
</root>
</configuration>
七、常见问题与解决方案
问题1:日志太多,磁盘空间不足
解决方案:
- 设置合理的日志保留策略(按天归档,保留30天)
- 使用日志轮转(大小到达阈值后自动切割)
- 区分业务日志和调试日志,分别存储
问题2:多线程下日志混乱
解决方案:
- 使用MDC(Mapped Diagnostic Context)添加线程ID或请求ID
- 对于分布式系统,传递traceId实现全链路追踪
问题3:敏感信息泄露
解决方案:
- 对敏感字段进行脱敏处理
- 配置日志过滤器,自动屏蔽敏感关键词
- 建立日志审计机制
八、总结
优质的日志应该具备以下特点:
- 适度 - 不多不少,刚好够用
- 规范 - 格式统一,便于检索
- 结构化 - 便于机器解析和分析
- 有上下文 - 能够还原问题场景
- 高性能 - 不影响业务逻辑的执行
记住:日志是你和系统之间的桥梁,用心写好每一行日志,当系统出现问题时,它会成为你最可靠的帮手。