谈谈日志规范

一、背景

编码过程中日志的重要性不言而喻,开发时通过打日志可以帮助调试代码,生产环境可以通过日志记录异常信息,排查问题等。一个好的日志规范同样重要,随意的打日志可能造成一些重要的异常堆栈信息丢失,甚至会占用大量的磁盘IO造成程序性能问题。

二、日志技术选型

建议统一使用slf4j
因为它是使用门面模式的日志框架,便于我们后期随时切换日志实现。避免在代码中直接使用log4j或java logging 等实现类。
实现方式统一使用Logback框架,具体原因可以参考这篇文章《logback最佳实践》。

对象声明
建议使用private static final。声明为private可防止logger对象被其他类非法使用。声明为static可防止重复new出logger对象,还可以防止logger被序列化,造成安全风险。声明为final是因为在类的生命周期内无需变更logger。

private static final Logger logger = LoggerFactory.getLogger(MailService.class);

如果觉得麻烦的话,也可以直接在类上打上lombok的 @Slf4j注解,它会帮我们自动生成代码。

三、日志级别的选择

ERROR
影响到程序正常运行、当前请求正常运行的异常情况:

  1. 打开配置文件失败
  2. 所有第三方对接的异常(包括第三方返回错误码)
  3. 所有影响功能使用的异常,包括:SQLException和除了业务异常之外的所有异常(RuntimeException和Exception)

WARN
不应该出现但是不影响程序、当前请求正常运行的异常情况:

  1. 有容错机制的时候出现的错误情况
  2. 找不到配置文件,但是系统能自动创建配置文件
  3. 即将接近临界值的时候,比如缓存池占用达到警告线
  4. 业务异常的记录

INFO
记录系统关键信息:

  1. Service方法中对于系统/业务状态的变更
  2. 主要逻辑中的分步骤,if/else等
  3. 客户端请求参数(REST/WS)
  4. 调用第三方接口时的调用参数和调用结果

DEBUG
可以将各类详细信息记录到DEBUG里,起到调试的作用,包括参数信息,调试细节信息,返回值信息等。
注意:生产环境需要关闭DEBUG信息

TRACE
更详细的跟踪信息,这个基本用不到。

述日志级别从高到低排列,是开发中最常用的五种。生产系统一般只打印INFO 级别以上的日志,对于 DEBUG 级别的日志,只在测试环境中打印。打印错误日志时,需要区分是业务异常(如:用户名不能为空)还是系统异常(如:调用 会员核心异常),业务异常使用 warn 级别记录,系统异常使用 error 记录

四、正确姿势

4.1 语言

最好在打印日志时输出英文,防止中文不支持而打印出乱码的情况。

4.2 必须使用参数化信息的方式

logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);

4.3 对于debug日志,必须判断是否为debug级别后,才进行使用。

如果不加判断,即使当前日志级别是INFO,字符串拼接操作依然会进行。

if (logger.isDebugEnabled()) {
   logger.debug("Processing trade with id: " +id + " symbol: " + symbol);
}

如果打印的实参不含计算,则没必要加这个判断,具体可参考《关于打印debug日志是否加判断日志级别的分析》。

4.4 禁止使用字符串拼接

使用字符串拼接会产生很多String对象,占用空间,影响性能,而且使用字符串拼接的可读性和可维护性都比较差,建议使用占位符。

// 错误
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);

// 正确
logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);

4.5 建议使用[]进行参数变量隔离

这样的格式写法,可读性更好,对于排查问题更有帮助。

4.6 循环体内不要打印 INFO 级别日志

    for (Person person : personList){
            log.info("the person is [{}]", JSON.toJSONString(person));
        }

4.7 if..else判断

对于else 是非正常的情况,需要根据情况选择打印warn 或 error 日志。对于只有 if 没有 else 的地方,如果 else 的路径是不可能的,应当加上 else 语句,并打印 error 日志。

    if (true){
            // do something
        }else {
            log.error("It is impossible...");
        }

同时对于if…else 或者 switch这样的分支,要在分支的首行打印日志,用来确定进入了哪个分支。

4.8 不打印无意义日志

不记录对于排查故障毫无意义的日志信息,日志信息一定要带有业务信息。

//错误
log.error("Consume message failed!");
//正确
log.error("Consume message failed,msgId={}",id);

4.9 打印日志的代码任何情况下都不允许失败

一定要确保不会因为Log语句的问题而抛出异常造成中断。如下,如果request为null,就会抛空指针异常。

log.error("execute failed,id:[{}]",request.getId());

4.10 远程调用接口建议打印日志

比如使用Fegin调用库存服务的接口,需要记录方法入参和返回值,对于排查问题会有很大帮助。

log.info("prepare to batch query rfid,businessId:[{}],productIds:[{}]",businessId,productIds);

List<InvProductRfid> rfids = invProductClient.batchQueryRfidByProductId(businessId, productIds);
log.info("batch query rfid result:[{}]", JSON.toJSONString(rfids));

4.11 异常的处理

catch中的异常记录必须打印堆栈信息,不要用e.printStackTrace()。

try {
            // do something
        }catch (Exception e){
            // 正确
            log.error("something wrong",e);
            // 错误,丢失了堆栈信息
            log.error("something wrong,errorMsg:[{}]",e.getMessage());
            // 错误
            e.printStackTrace();
        }

如果进行了抛出异常操作,请不要记录error日志,由最终处理方进行处理。

反例(不要这么做):

try{
    ....
}catch(Exception ex){
  String errorMessage=String.format("Error while reading information of user [%s]",userName);
  logger.error(errorMessage,ex);
  throw new UserServiceException(errorMessage,ex);
}

4.12参数校验错误打印WARN日志

posted @ 2019-12-01 11:41  烟味i  阅读(777)  评论(0编辑  收藏  举报