代码中如何打印优质的日志

前言

日志是程序的"黑匣子",它记录了程序运行时的关键信息。在实际开发中,良好的日志体系能帮助我们快速定位问题、分析系统性能、监控业务状态。然而,很多开发者在编写日志时存在随意性,导致日志要么冗余混乱,要么信息不足。

本文将分享如何编写高质量的日志,从基本原则、日志级别、最佳实践等方面进行详细说明。

一、日志的基本原则

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:敏感信息泄露

解决方案

  • 对敏感字段进行脱敏处理
  • 配置日志过滤器,自动屏蔽敏感关键词
  • 建立日志审计机制

八、总结

优质的日志应该具备以下特点:

  1. 适度 - 不多不少,刚好够用
  2. 规范 - 格式统一,便于检索
  3. 结构化 - 便于机器解析和分析
  4. 有上下文 - 能够还原问题场景
  5. 高性能 - 不影响业务逻辑的执行

记住:日志是你和系统之间的桥梁,用心写好每一行日志,当系统出现问题时,它会成为你最可靠的帮手。

相关推荐
用户6802659051191 小时前
全栈可观测性白皮书——实施、收益与投资回报率
javascript·后端·面试
天若有情6732 小时前
IoC不止Spring!求同vs存异,两种反向IoC的核心逻辑
java·c++·后端·算法·spring·架构·ioc
神奇小汤圆2 小时前
给 Spring Boot 接口加了幂等保护:Token 机制 + 结果缓存,一个注解搞定
后端
绝无仅有2 小时前
mac笔记本中在PHP中调用Java JAR包的指南
后端·面试·架构
绝无仅有2 小时前
PHP与Java项目在服务器上的对接准备与过程
后端·面试·架构
神奇小汤圆2 小时前
理解 SQL JOIN: ON 与 WHERE 的区别
后端
四七伵2 小时前
数据库必修课:MySQL金额字段用decimal还是bigint?
数据库·后端
彭于晏Yan2 小时前
LangChain4j实战三:图像模型
java·spring boot·后端·langchain
SimonKing2 小时前
跨越数据孤岛!SpringBoot使用JDBC调用Calcite联邦查询实战
java·后端·程序员