谈谈日志规范
一、背景
编码过程中日志的重要性不言而喻,开发时通过打日志可以帮助调试代码,生产环境可以通过日志记录异常信息,排查问题等。一个好的日志规范同样重要,随意的打日志可能造成一些重要的异常堆栈信息丢失,甚至会占用大量的磁盘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
影响到程序正常运行、当前请求正常运行的异常情况:
- 打开配置文件失败
- 所有第三方对接的异常(包括第三方返回错误码)
- 所有影响功能使用的异常,包括:SQLException和除了业务异常之外的所有异常(RuntimeException和Exception)
WARN
不应该出现但是不影响程序、当前请求正常运行的异常情况:
- 有容错机制的时候出现的错误情况
- 找不到配置文件,但是系统能自动创建配置文件
- 即将接近临界值的时候,比如缓存池占用达到警告线
- 业务异常的记录
INFO
记录系统关键信息:
- Service方法中对于系统/业务状态的变更
- 主要逻辑中的分步骤,if/else等
- 客户端请求参数(REST/WS)
- 调用第三方接口时的调用参数和调用结果
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日志