文章目录
日志输出指南
一、入参和出参
日志应该为统一对入参和出参进行记录,方便线上排查问题
具体实现方式的话有三种方式
- 通过 HandlerInterceptor 的方式进行拦截
- 通过 Spring 的切面
- 通过 Filter
三种效果实际上都差不多了,性能得话 Filter 最高、切面最低、HandlerInterceptor 居中,但是性能也差不了多少哪种习惯用那种
调用链路是 请求先过 Filter -> HandlerInterceptor -> 切面
日志数据存入 ELK
ELK 的 Logstash 需要和运维沟通修改解析
教程一:日志接口级统一出入参实现
二、日志级别
我们主要用的分为四个级别
- Debug
- Info
- Warn
- Error
如果级别指定的够清晰的话,那么我们是可以通过 logback.xml
在线上对不需要的日志进行排查,让其不打印且不输出到控制台的
2.1、Debug 级别的定义和例子
在开发、调试和测试阶段的时候使用的消息级别,正式环境不打印。包括参数信息、调试细节、返回值信息等等。
例如:
log.debug("[正常请求]{}", uniqueId);
2.2、Info 级别的定义和例子
Info 日志主要记录系统关键信息,旨在保留系统正常工作期间关键运行指标。
例如:
这个就是启动日志里面 Sentinel 输出的日志
就比如说我们在程序的运行过程中,有一个类 A 里面有一个参数 Type 我们通过启动的时候,通过配置文件去指定不同的 Type 值来区分 A 类的执行流程,这种情况下,在项目启动时就应该将 TYPE 的值给输出出来,因为我有可能没那么方便去看正式环境的 Nacos 中的配置
2.3、Warn 级别的定义和例子
Warn 级别的主要输出警告性质的内容,这些内容是可以预知且是有规划的。
例如:
log.warn("请求的 username 格式不正确{}", username);
比如说这种判断格式是否正确的,就可以用 warn 来进行输出,主要是可以提前预知,比如说参数为 null 等
2.4、Error 级别的定义和例子
Error 级别主要针对于一些不可预知的信息,诸如:错误、系统异常等。
例如:
try{
//业务代码
} catch (Exception ex) {
log.error("系统异常 = ",ex);
}
比如说在调用远程服务的时候,就可能出现我们并不清楚的异常,比如说网络问题,比如说业务逻辑问题等等
error 和 warn 的区别可以简单理解为,warn 会去输出我们自己定义的业务异常,而 error 更多的情况是非我们自己手动抛出的系统异常
三、日志规范
3.1、不要输出无意义的日志
尽量在输出日志的时候,将一定的关键参数也带上,不然即使是测试时你知道这里出现异常,但是也还得去进行调试才能定位到问题的所在。
反例:
log.error("xxx 类 xxx 接口 报错!!!");
正例:
log.error("xxx 类 xxx 接口 报错, msgId:{}",id);
就比如说反例的例子,我确实看到了有报错,但是我并不清楚为什么报错,我还是得通过大量的排查来解决。
3.2、使用占位符而不是拼接
这个主要是性能问题,占位符的性能远大于字符串拼接
反例:
log.error("xxx 类 xxx 接口 报错, msgId:" + id);
正例:
log.error("xxx 类 xxx 接口 报错, msgId:{}",id);
3.3、尽量使用日志级别开关
这个还是性能问题
反例:
log.debug("xxx 类 xxx 接口 报错, user:{}" , JSON.toJSONString(user));
正例:
if (log.isDebugEnabled()) {
log.debug("xxx 类 xxx 接口 报错, user:{}" , JSON.toJSONString(user));
}
这里的问题主要在于
虽然 Logback 在代码中对日志级别进行了判断,如果不满足级别则直接 return
但是占位符这个时候实际上可能已经替换好了
这样就会浪费性能
就比如说上面的例子,在传入到替换之前的时候已经提前做了 JSON.toJSONString(user)
的转换,然后进入之后判断不满足日志输出级别,然后 return
出去,但是这个时候,无论有没有输出日志,实际上你已经转换了,性能已经损耗了
但是如果你使用
log.error("xxx 类 xxx 接口 报错, msgId:{}",id);
这种方式的话,就可以不用提前判断了,因为他不涉及到转换的问题,会在判断结束之后再去调用 toString()
方法进行占位符的替换。
当然如果你用很神奇的写法,那就得提前判断一下了
比如说:
log.debug.(String.format ("xxx 类 xxx 接口 报错, msgId:{}" , id ));
3.4、别使用系列
不要用这些输出
- System.out
- System.err
- e.printStackTrace()
别问,问就是懂的都懂
3.5、输出日志时尽量不要使用转换工具
比如说如果你使用了 JSON 工具来进行转换,一个是性能低,还有一个问题是如果这个类的 get 方法被重写了,那么就会导致异常的产生
因为在日志输出的时候会去调用对象的 toString()
进行输出,所以基本上是不需要进行转换输出的
写法为:
log.debug("xxx 类 xxx 接口 报错, user:{}" , user);
不需要你手动去调用 toString()
,如果手动调用的话,会在日志运行级别判断之前就损耗了性能,参照 3.3 条的问题
如果有需求非要转换输出,那么也参照 3.3 条的正例
3.6、e.getMessage() 和 e 的区别
在输出系统异常时,应该采用 e 来进行输出
比如说:
log.error("系统异常 = ",e);
e 是可以省略占位符的,如果你采用占位符的话,那么等同于 e.toString
具体情况参照 3.8 条
在输出自定义异常时,只需要采用 e.getMessage()
比如说:
log.warn("用户名或密码错误 : {}", e.getMessage());
这里的主要问题是如果是系统异常的话,我们是需要知道堆栈信息,方便去看异常出现处的,但是如果是自定义的业务异常的话,很多时候我们对他的堆栈并不感兴趣。
3.7、尽量不要重复打印日志
反例:
log.warn("用户名或密码错误 : {}", e.getMessage());
log.warn("该用户的用户名为 : {}", user.getUserName());
log.warn("该用户的密码为 : {}", user.getPasswrod());
这些日志应该被封装为一个
正例:
log.warn("用户名或密码错误 用户名: {}, 密码:{}, 异常信息:{}", user.getUserName() , user.getPasswrod() , e.getMessage());
3.8、输出 Throwable 时,可以不使用占位符的几种情况
可以看到只有两种情况是可以不使用占位符的,其他情况是需要使用的
3.9、对日志信息进行脱敏处理
后面想起来了再更新这块