Skip to content

日志规范

1. 背景

通常,Java 程序员在开发项目时都是依赖 Eclipse/IDEA 等集成开发工具的 Debug 调试功能来跟踪解决 Bug,但项目发布到了测试、生产环境并不能这么做。所以,日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的手段。

日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性。

2. 目的

  • 问题追踪:辅助排查和定位线上问题,优化程序运行性能;
  • 状态监控:通过日志分析,可以监控系统的运行状态;
  • 安全审计:审计主要体现在安全上,可以发现非授权的操作;
  • 业务指导:通过统计和分析相关的业务、用户行为日志,对业务进行预测指导。

4. 记录日志的时机

在看线上日志的时候,我们可曾陷入到日志泥潭?该出现的日志没有,无用的日志一大堆,或者需要的信息分散在各个角落,特别是遇到紧急的在线 bug 时,有效的日志被大量无意义的日志信息淹没,焦急且无奈地浪费大量精力查询日志。那什么是记录日志的合适时机呢?

总结几个需要写日志的点:

  • 编程语言提示异常

如今各类主流的编程语言都包括异常机制,业务相关的流行框架有完整的异常模块。这类捕获的异常是系统告知开发人员需要加以关注的,是质量非常高的报错。应当适当记录日志,根据实际结合业务的情况使用 warn 或者 error 级别。

  • 业务流程预期不符

除开平台以及编程语言异常之外,项目代码中结果与期望不符时也是日志场景之一,简单来说所有流程分支都可以加入考虑。取决于开发人员判断能否容忍情形发生。常见的合适场景包括外部参数不正确,数据处理问题导致返回码不在合理范围内等等。

  • 系统核心角色,组件关键动作

系统中核心角色触发的业务动作是需要多加关注的,是衡量系统正常运行的重要指标,建议记录 INFO 级别日志,比如电商系统用户从登录到下单的整个流程;微服务各服务节点交互;核心数据表增删改;核心组件运行等等,如果日志频度高或者打印量特别大,可以提炼关键点 INFO 记录,其余酌情考虑 DEBUG 级别。

  • 系统初始化

系统或者服务的启动参数。核心模块或者组件初始化过程中往往依赖一些关键配置,根据参数不同会提供不一样的服务。务必在这里记录 INFO 日志,打印出参数以及启动完成态服务表述。

5. 日志规范

5.1 日志变量定义

日志变量往往不变,最好定义成 final static,变量名用大写。

java
private static final Logger log = LoggerFactory.getLogger(SimpleClassName.getClass());

通常一个类只有一个 log 对象,如果有父类可以将 log 定义在父类中。

推荐引入 lombok 的依赖,在类的头部加上 @Slf4j 的注解,之后便可以在程序的任意位置使用 log 变量打印日志信息了,使用起来更加简洁一点,在重构代码尤其是修改类名的时候无需改动原有代码。

5.2 参数占位格式

  • 使用参数化形式 {} 占位,[] 进行参数隔离
java
log.debug("Save order with order no:[{}], and order amount:[{}]");
log.debug("Save order with order no:[{}], and order amount:[{}]");

这种可读性好,这样一看就知道 [] 里面是输出的动态参数,{} 用来占位类似绑定变量,而且只有真正准备打印的时候才会处理参数,方便定位问题。

  • 如果日志框架不支持参数化形式,且日志输出时不支持该日志级别时会导致对象冗余创建,浪费内存,此时就需要使用 isXXEnabled 判断,如:
java
if (log.isDebugEnabled()) {
    // 如果日志不支持参数化形式,debug又没开启,那字符串拼接就是无用的代码拼接,影响系统性能
    log.debug("Save order with order no:"+orderNo +", and order amount:"+orderAmount);
}

至少 debug 级别是需要开启判断的,线上日志级别至少应该是 info 以上的。

这里推荐大家用 SLF4J 的门面接口,可以用参数化形式输出日志,debug 级别也不必用 if 判断,简化代码。

5.3 日志内容

  • 禁用 System.out.printlnSystem.err.println
  • 输出日志的对象,应在其类中实现快速的 toString() 方法,以便于在日志输出时仅输出这个对象类名和 hashCode()
  • 预防空指针:不要在日志中调用对象的方法获取值,除非确保该对象肯定不为 null,否则很有可能会因为日志的问题而导致应用产生空指针异常。
java
// 不推荐
log.debug( "Load student(id={}), name: {}",id, student.getName() );

// 推荐
log.debug( "Load student(id={}), student: {}",id, student);
  • 避免输出拼接日志,对于一些一定需要进行拼接字符串,或者需要耗费时间、浪费内存才能产生的日志内容作为日志输出时,应使用 log.isXxxxxEnable() 进行判断后再进行拼接处理,比如:
java
if (log.isDebugEnable()) {
    StringBuilder builder = new StringBuilder();

    for (Student student: students) {
        builder.append("student: ").append(student);
    }
    
    builder.append("value: ").append(JSON.toJSONString(object));
    
    log.debug( "debug log example, detail: {}",builder);
}

5.4 exception 日志

  • 输出 Exceptions 的全部 Throwable 信息;因为 log.error(msg)log.error(msg,e.getMessage()) 这样的日志输出方法会丢失掉最重要的 StackTrace 信息。
java
void foo() {
    try {
        //do somehing 
    } catch (Exception e) {
      //错误示范
      log.error(e.getMessage());
      //错误示范
      log.erroe("Bad Things", e.getMessage());
      //正确演示
      log.error("Bad Things", e);
    }
}
  • 不允许记录日志后又抛出异常。如捕获异常后又抛出了自定义业务异常,此时无需记录错误日志,由最终捕获方进行异常处理。不能又抛出异常,又打印错误日志,不然会造成重复输出日志。
java
void foo() throws LogException {
    try {
        //do somehing 
    } catch (Exception e) {
        //正确
        log.error("Bad Things", e);
        throw new LogException("Bad Things", e);
    }
}

5.5 禁止项

  • 禁止 System.out.println()System.error.println() 语句;
  • 禁止出现 printStackTrace()
  • 禁止大循环中打印日志 例如:
java
for (int i = 0; i < 2000; i++) {
    log.info("XX");
}
  • 禁止在线上环境开启 debug 级别日志输出。