← 返回文章列表

日志规范最佳实践

2025/8/15非最佳实践#Java#日志

日志规范最佳实践

一、日志规范问题及改进方案

1. 异常处理规范

❌ 错误示例

// 1. 生吞异常,只打印toString
catch (Exception e) {
    log.error("查询错误:" + e);  // 丢失堆栈信息
}

// 2. 空catch块
catch (Exception e) {
    // 什么都不做 - 严重问题!
}

// 3. 使用printStackTrace
catch (Exception e) {
    e.printStackTrace();  // 输出到System.out,不受日志框架管理
}

// 4. 字符串拼接
catch (Exception e) {
    log.error("查询模板错误: " + templateName + ", 异常: " + e);
}

✅ 正确示例

// 1. 完整记录异常堆栈
catch (Exception e) {
    log.error("查询数据库失败,module: {}, template: {}", module, templateName, e);
}

// 2. 明确说明忽略原因
catch (Exception e) {
    // 此异常为预期行为,用户取消操作
    log.debug("用户取消操作,忽略异常", e);
}

// 3. 使用日志框架
catch (Exception e) {
    log.error("ResultSet映射失败,表名: {}", tableName, e);
}

// 4. 使用占位符
catch (Exception e) {
    log.error("查询模板错误,templateName: {}, userId: {}", templateName, userId, e);
}

2. 日志级别使用规范

级别使用场景示例
ERROR系统异常、需要人工介入数据库连接失败、外部服务不可用
WARN业务异常、性能问题参数校验失败、慢查询、降级处理
INFO关键业务流程用户登录、订单创建、支付成功
DEBUG调试信息方法入参出参、SQL 语句、中间状态
TRACE详细追踪循环内部状态、详细计算过程

❌ 错误示例

// 异常用INFO级别
try {
    // ...
} catch (Exception e) {
    log.info("系统错误:" + e);  // 应该用ERROR
}

// 调试信息用ERROR级别
log.error("进入方法:{}", methodName);  // 应该用DEBUG

✅ 正确示例

// 系统异常用ERROR
catch (SQLException e) {
    log.error("数据库查询失败,SQL: {}", sql, e);
}

// 业务异常用WARN
if (user == null) {
    log.warn("用户不存在,userId: {}", userId);
}

// 关键流程用INFO
log.info("用户登录成功,userId: {}, loginTime: {}", userId, loginTime);

// 调试信息用DEBUG
log.debug("方法入参,method: {}, params: {}", methodName, params);

3. 日志内容规范

必须包含的信息

  1. What - 发生了什么
  2. Where - 在哪里发生(类、方法、行号)
  3. When - 什么时候发生
  4. Who - 谁触发的(用户 ID、请求 ID)
  5. Why - 为什么发生(异常原因)
  6. How - 如何处理(降级、重试、忽略)

❌ 错误示例

log.error("失败");  // 信息太少
log.error("查询失败" + e.getMessage());  // 缺少上下文
log.info("result=" + result.toString());  // 可能NPE

✅ 正确示例

// 包含完整上下文
log.error("查询用户订单失败,userId: {}, orderId: {}, 耗时: {}ms",
    userId, orderId, costTime, e);

// 防御性编程
log.info("查询结果,count: {}, data: {}",
    result != null ? result.size() : 0,
    result);

// 使用工具类
LoggerUtil.logSystemError(log,
    "数据库连接失败,尝试重连", e,
    "database", dbName, "retry", retryCount);

4. 性能优化

❌ 错误示例

// 1. 不必要的字符串拼接
log.debug("Heavy object: " + expensiveToString());  // 即使DEBUG关闭也会执行

// 2. 重复日志
for (User user : users) {
    log.info("Processing user: {}", user);  // 可能打印上千条
}

✅ 正确示例

// 1. 使用占位符或判断
if (log.isDebugEnabled()) {
    log.debug("Heavy object: {}", expensiveToString());
}

// 2. 批量记录
log.info("开始处理用户,总数: {}", users.size());
// 处理逻辑...
log.info("用户处理完成,成功: {}, 失败: {}", successCount, failCount);

二、实战示例改进

改进前后对比

示例 1:DmService.queryDb

// ❌ 原代码
public List<Map<String, Object>> queryDb(parameter, queryDetail, isQueryDb) {
    try {
        // ...
    } catch (Exception e) {
        log.error("[getSqlAndQuery]查询报错");  // 没有异常信息!
    }
    return new ArrayList<>();
}

// ✅ 改进后
public List<Map<String, Object>> queryDb(parameter, queryDetail, isQueryDb) {
    String module = (String) parameter.get(MODULE);
    String requestId = LoggerUtil.getRequestId();

    try {
        log.debug("开始查询,module: {}, requestId: {}", module, requestId);
        List<Map<String, Object>> result = service.getSqlAndQuery(parameter, queryDetail, isQueryDb);
        log.debug("查询成功,module: {}, size: {}", module, result.size());
        return result;
    } catch (Exception e) {
        log.error("查询失败,module: {}, requestId: {}, params: {}",
            module, requestId, parameter, e);
        throw new BizException("查询失败", e);
    }
}

示例 2:文件上传异常处理

// ❌ 原代码
catch (IOException ex) {
    log.error("合并文件失败," + ex);
    throw new RuntimeException("上传失败");
}

// ✅ 改进后
catch (IOException ex) {
    log.error("文件合并失败,fileName: {}, size: {}KB, userId: {}",
        fileName, fileSize/1024, userId, ex);
    // 保留原始异常信息
    throw new BizException("文件上传失败:" + ex.getMessage(), ex);
}

示例 3:慢查询监控

// ✅ 新增慢查询监控
long startTime = System.currentTimeMillis();
try {
    List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
    long costTime = System.currentTimeMillis() - startTime;

    // 记录慢查询
    if (costTime > 3000) {
        log.warn("慢查询警告,sql: {}, cost: {}ms, rows: {}",
            sql.substring(0, Math.min(sql.length(), 100)),
            costTime, result.size());
    }

    return result;
} catch (Exception e) {
    long costTime = System.currentTimeMillis() - startTime;
    log.error("SQL执行失败,sql: {}, cost: {}ms", sql, costTime, e);
    throw e;
}

三、日志配置建议

logback.xml 配置示例

<configuration>
    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{requestId}] [%X{userId}] - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 错误日志单独文件 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <file>logs/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger{36} [%X{requestId}] - %msg%n%ex</pattern>
        </encoder>
    </appender>

    <!-- 慢查询日志 -->
    <appender name="SLOW_SQL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/slow-sql.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 日志级别配置 -->
    <logger name="com.mgtv.mofang" level="INFO"/>
    <logger name="com.mgtv.mofang.service.dw" level="DEBUG"/>
    <logger name="SLOW_SQL" level="INFO" additivity="false">
        <appender-ref ref="SLOW_SQL"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>
</configuration>

四、检查清单

代码审查检查项

  • 所有 catch 块都有日志记录
  • 异常日志包含完整堆栈(最后一个参数是 Throwable)
  • 使用占位符而非字符串拼接
  • 日志级别使用正确
  • 包含足够的上下文信息
  • 没有敏感信息(密码、密钥等)
  • 循环中避免重复日志
  • DEBUG 日志有 isDebugEnabled 判断

常见错误速查

  1. log.error(msg + e)log.error(msg, e)
  2. e.printStackTrace()log.error(msg, e)
  3. catch(Exception e) {}catch(Exception e) { log.error(msg, e); }
  4. log.info("error: " + e)log.error(msg, e)
  5. "param=" + param"param: {}"

五、总结

良好的日志是系统可观测性的基础。遵循这些最佳实践可以:

  1. 快速定位问题 - 完整的异常堆栈和上下文
  2. 监控系统健康 - 合理的日志级别和告警
  3. 优化性能 - 发现慢查询和性能瓶颈
  4. 保障安全 - 避免敏感信息泄露

记住:日志是写给未来的自己和同事看的,请善待他们!