不仅是初学者,很多老码农也不能很好的打印日志(注意 : 技术大佬不是本文的目标读者),本质的原因是没有意识到为什么要打印日志,打印日志的目的是什么,这里我就为大家介绍一下。
无论做什么事情,都要搞明白为什么要做,才能做的更好!思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
日志打印通常有四种级别,从高到低分别是:ERROR、WARN、INFO、DEBUG。
ERROR:该级别的错误需要马上被处理,当ERROR错误发生时,已经影响了用户的正常访问,是需要马上介入并处理的。常见的ERROR异常有:空指针异常,数据库不可用,服务不可用,关键用例无法继续执行等。
ERROR是错误的意思,但不代表出现异常的地方就该打ERROR。
ERROR是相对程序正确运行来说的,如果出现了ERROR那就代表出问题了,开发人员必须要查一下原因,或许是程序问题,或许是环境问题,或许是理论上不该出错的地方出错了。总之,如果你觉得某个地方出问题时需要解决,就打ERROR,如果不需要解决就不要打ERROR。
如,接口入参检查就不应该使用ERROR级别日志,甚至都不应该打印日志,而是直接返回错误码,交由上层调用方处理。
否则频繁的告警会使运维,开发对错误告警麻木,无效的告警会将真正有问题的错误淹没掉。
WARN:不应该出现但是不影响程序、当前请求正常运行的异常情况。对于WARN级别的日志,虽然不需要管理员马上处理,但是也需要引起重视。
WARN日志有两种级别:一个是解决方案存在明显的问题(例如Try Catch语句中由于不确定会出现什么异常,而用Exception统一捕获抛出的问题),另一个是潜在的问题和建议(例如系统性能可能会伴随着时间的迁移逐渐不能满足服务需要,或者非核心接口的偶尔超时)。应用程序可以容忍这些信息,不过它们应该被及时地检查及修复。
如果WARN出现的过于频繁或次数太多,那就代表你要检查一下程序或环境或依赖程序是否真的出问题了。此时应该监控warn日志的次数,达到阈值告警并处理。
INFO:主要用于记录系统运行状态、核心用例执行结果、用户的操作行为等信息。该日志级别,常用于反馈系统当前状态给最终用户。
DEBUG:该级别日志的主要作用是对系统每一步的运行状态进行精确的记录。通过该种日志,可以查看某一个操作每一步的执行过程,可以准确定位是何种操作,何种参数,何种顺序导致了某种错误的发生。可以保证在不重现错误的情况下,也可以通过DEBUG级别的日志记录对问题进行诊断。
一般来说,在生产环境中,不会输出该级别的日志。
打印日志的位置
通用信息
级别,例如 warn、info 和 error
时间
进程ID
线程ID:线程ID相当重要,在解决多线程问题的时候,这一项信息是必不可少的
模块:哪个类,假如是分布式服务的话,需要指明是哪个服务的哪个类
Filter:主要用于查找某一类LOG时候方便,这个Filter是开发人员在开发过程中自己定义的,可以是方法名/业务模块名称/功能名称等等。
请求id: 一个请求到来会经过很多服务的很多方法,一个唯一的请求id可以将该请求的所有日志串联起来
耗时:可以通过拦截器或AOP来打印某服务或某核心方法的耗时,可以使用一些agent来拦截方法执行时间。
如某公司通用日志打印
[INFO] 2020-11-26 14:06:59.330[ConsumeMessageThread_56][2011280MD13623573890648168927232] c.w.w.r.h.SyncRequestHandler:108
入参出参:入参和出参不是每个地方都要打印的,只在服务对外暴露的API/核心方法提供即可。注意打印的参数一定要是原始参数,而不是经过转换后的参数,防止你参数转换的代码有问题。
日志格式:日志格式统一使用主语+谓语+宾语+状语的格式,日志打印应该遵从人类的自然语言,任何没有开发经验或是外行人都能从你的日志中捕获到有用的信息。使用 [] 进行参数变量隔离,这样的格式写法,可读性更好,对于排查问题更有帮助
java生态中有很多日志框架可供选择,如log4j, log4j2, logback等等。他们各自的特点不是本文重点,请读者自行google。
这里需要强调一点:
应用中不可直接使用日志系统( Log4j、 Logback)中的 API,而应依赖使用日志框架SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
任何系统都有很多的接口和方法,首先要根据业务划分优先级,指定不同的业务级别。
除了上面降到的4种基本的告警级别外,还要根据业务级别划分不同的告警级别。
举个例子,比如接口超时,如果是核心业务,需要打印ERROR日志,并且出现一次就要报警;如果不是核心业务,那么可能只需要打印warn日志,然后配置在一定时间窗口内出现N次才报警处理。
服务端不可以相信前端任何输入。需要对入参做严格都校验。
微服务间,下层服务也需要对上层服务都入参做校验,不要以为调用方都是一个部门或公司的就忽略了
对于批量操作,务必对请求都数据量做校验,避免大SQL或内存撑爆
日志的打印需要对用户的身份证,密码等敏感信息脱敏。
微服务系统需要配合链路追踪和ELK等工具。
如下, 已经有抛出异常了,那么没必要再打印一次error日志。但是,要注意将具体的错误信息封装成自然语言抛出异常,并且上层要捕获并打印error日志。
try {
//do sth
} catch (Exception e) {
logger.error('XX 发生异常', e);
throw new IOException();
}
反例:未使用占位符
log.error("具体业务信息,入参:"+sceneId);
反例:日志没有有效信息,没有堆栈信息
try {
throw new Exception("测试日志");
}catch (Exception e){
logger.info(e.getMessage());//错误
logger.info("输入日志1",e.getMessage());//错误
logger.info("输入日志2",e);//正确
}
如下:没有打印status的值。如果分支比较复杂,最好在status=1和2两个分支打印一下走向。当日在外层打印也行,只要能快速定位到走的哪个分支流程。
if(status=1){
//do sth
}else if(status=2){
//do sth
}else{
log.warn("status 有误");
}
反例:日志未能准确表达错误信息
Connection connection =ConnectionFactory.getConnection();
if (connection == null) {
log.warn("System initialized unsuccessfully");
}
反例:由于打印日志,导致新的异常。通常是空指针异常
log.debug("Processing request with id: {}", request.getId());
反例:将数据库中的数据全部打印出来
有时候只需要打印数据量大小即可,如果打印了很多无用的数据,不仅性能损耗,占用额外的磁盘空间,还会给日志查看带来困扰
反例:错误的打印对象
如下,如果parama是对象,那么可能打印的是对象的hashcode值。可以重写tostring方法或者转为json打印,推荐json
logger.error("calculate user fee error. parama=[{}], paramb=[{}]", parama, paramb, e)
正例:
logger.debug("UserName is:[{}] and age is : [{}] ", name, age);
logger.error("calculate user fee error. parama=[{}], paramb=[{}]", json(parama), paramb, e)
原文:https://blog.csdn.net/liubenlong007/article/details/110262961