关于java:Java-日志记录最佳实践写得太好了吧

38次阅读

共计 9652 个字符,预计需要花费 25 分钟才能阅读完成。

作者:GeekerLou\
起源:jianshu.com/p/546e9aace657

一、日志简介

1.1 日志是什么(WHAT)

日志:记录程序的运行轨迹,不便查找要害信息,也不便疾速定位解决问题。

通常,Java 程序员在开发我的项目时都是依赖 Eclipse/IDEA 等集成开发工具的 Debug 调试性能来跟踪解决 Bug,但我的项目公布到了测试、生产环境怎么办?你有可能会说能够应用近程调试,但理论并不能允许让你这么做。

所以,日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的伎俩。日志打得好,就能依据日志的轨迹疾速定位并解决线上问题,反之,日志输入不好,不仅无奈辅助定位问题反而可能会影响到程序的运行性能和稳定性。

很多介绍 AOP 的中央都采纳日志来作为介绍,实际上日志要采纳切面的话是极其不迷信的!对于日志来说,只是在办法开始、完结、异样时输入一些什么,那是相对不够的,这样的日志对于日志剖析没有任何意义。如果在办法的开始和完结整个日志,那办法中呢?如果办法中没有日志的话,那就齐全失去了日志的意义!如果利用呈现问题要查找由什么起因造成的,也没有什么作用。这样的日志还不如不必!

1.2 日志有什么用(WHY)

不论是应用何种编程语言,日志输入简直无处不再。总结起来,日志大抵有以下几种用处:

  • 「问题追踪」:辅助排查和定位线上问题,优化程序运行性能。
  • 「状态监控」:通过日志剖析,能够监控零碎的运行状态。
  • 「平安审计」:审计次要体现在平安上,能够发现非受权的操作。

1.3 总结

日志在应用程序中是十分十分重要的,好的日志信息能有助于咱们在程序呈现 BUG 时能疾速进行定位,并能找出其中的起因。

作为一个有涵养的程序猿,对日志这个货色该当引起足够的器重。

二、日志框架(HOW)

2.1 罕用的日志框架

log4j、Logging、commons-logging、slf4j、logback,开发的同学对这几个日志相干的技术不生疏吧,为什么有这么多日志技术,它们都是什么区别和分割呢?且看下文合成:

2.1.1 Logging

这是 Java 自带的日志工具类,在 JDK 1.5 开始就曾经有了,在 java.util.logging 包下。通常状况下,这个根本没什么人用了,理解一下就行。

2.1.2 commons-logging

commons-logging 是日志的门面接口,它也是 Apache 最早提供的日志门面接口,用户能够依据爱好抉择不同的日志实现框架,而不用改变日志定义,这就是日志门面的益处,合乎面对接口形象编程。当初曾经不太风行了,理解一下就行。

2.1.3 Slf4j

slf4j, 英文全称为“Simple Logging Facade for Java”,为 java 提供的简略日志 Facade。Facade 门面,更底层一点说就是接口。它容许用户以本人的爱好,在工程中通过 slf4j 接入不同的日志零碎。

因而 slf4j 入口就是泛滥接口的汇合,它不负责具体的日志实现,只在编译时负责寻找适合的日志零碎进行绑定。具体有哪些接口,全副都定义在 slf4j-api 中。查看 slf4j-api 源码就能够发现,外面除了 public final class LoggerFactory 类之外,都是接口定义。因而 slf4j-api 实质就是一个接口定义。

2.1.4 Log4j

Log4j 是 Apache 的一个开源日志框架,也是市场占有率最多的一个框架。

留神:log4j 在 2015.08.05 这一天被 Apache 发表进行保护了,用户须要切换到 Log4j2 下面去。

上面是官宣原文:

On August 5, 2015 the Logging Services Project Management Committee announced that Log4j 1.x had reached end of life. For complete text of the announcement please see the Apache Blog. Users of Log4j 1 are recommended to upgrade to Apache Log4j 2.

2.1.5 Log4j2

Log4j 2 Apache Log4j 2 是 apache 开发的一款 Log4j 的降级产品。

Log4j2 与 Log4j1 产生了很大的变动,log4j2 不兼容 log4j1。

2.1.6 Logback

Logback 是 Slf4j 的原生实现框架,同样也是出自 Log4j 一个人之手,但领有比 log4j 更多的长处、个性和更做强的性能,当初根本都用来代替 log4j 成为支流。

Logback 绝对于 log4j 领有更快的执行速度。基于咱们先前在 log4j 上的工作,logback 重写了外部的实现,在某些特定的场景下面,甚至能够比之前的速度快上 10 倍。在保障 logback 的组件更加疾速的同时,同时所需的内存更加少。

2.2 日志框架怎么选

选项太多了的结果就是抉择艰难症,我的认识是没有最好的,只有最合适的:

  • commons-loggin、slf4j 只是一种日志形象门面,不是具体的日志框架。log4j、logback 是具体的日志实现框架。
  • 在比拟关注性能的中央,抉择 Logback 或本人实现高性能 Logging API 可能更适合。举荐:slf4j + logback.
  • 在曾经应用了 Log4j 的我的项目中,如果没有发现问题,持续应用可能是更适合的形式:举荐组合为:slf4j + log4j2.
  • 如果不想有依赖则应用 java.util.logging 或框架容器曾经提供的日志接口。

三、记录日志的机会

在看线上日志的时候,咱们可曾陷入到日志泥潭?该呈现的日志没有,无用的日志一大堆,或者须要的信息扩散在各个角落,特地是遇到紧急的在线 bug 时,无效的日志被大量无意义的日志信息吞没,着急且无奈地节约大量精力查问日志。那什么是记录日志的适合机会呢?

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

  • 「编程语言提醒异样」:现在各类支流的编程语言都包含异样机制,业务相干的风行框架有残缺的异样模块。这类捕捉的异样是零碎告知开发人员须要加以关注的,是品质十分高的报错。该当适当记录日志,依据理论联合业务的状况应用 warn 或者 error 级别。
  • 「业务流程预期不符」:除开平台以及编程语言异样之外,我的项目代码中后果与冀望不符时也是日志场景之一,简略来说所有流程分支都能够退出思考。取决于开发人员判断是否容忍情景产生。常见的适合场景包含内部参数不正确,数据处理问题导致返回码不在正当范畴内等等。
  • 「系统核心角色,组件要害动作」:零碎中外围角色触发的业务动作是须要多加关注的,是掂量零碎失常运行的重要指标,倡议记录 INFO 级别日志,比方电商零碎用户从登录到下单的整个流程;微服务各服务节点交互;外围数据表增删改;外围组件运行等等,如果日志频度高或者打印量特地大,能够提炼关键点 INFO 记录,其余酌情思考 DEBUG 级别。
  • 「零碎初始化」:零碎或者服务的启动参数。外围模块或者组件初始化过程中往往依赖一些要害配置,依据参数不同会提供不一样的服务。务必在这里记录 INFO 日志,打印出参数以及启动实现态服务表述。

四、日志打印最佳实际

4.1 日志变量定义

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

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

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

日志变量类型定义为门面接口(如 slf4j 的 Logger),实现类能够是 Log4j、Logback 等日志实现框架,不要把实现类定义为变量类型,否则日志切换不不便,也不合乎形象编程思维。

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

4.2 参数占位格局

应用参数化模式 {} 占位,[]进行参数隔离

log.debug("Save order with order no:[{}], and order amount:[{}]");
log.debug("Save order with order no:[{}], and order amount:[{}]");

这种可读性好,这样一看就晓得 [] 外面是输入的动静参数,{}用来占位相似绑定变量,而且只有真正筹备打印的时候才会解决参数,不便定位问题。

如果日志框架不反对参数化模式,且日志输入时不反对该日志级别时会导致对象冗余创立,节约内存,此时就须要应用 isXXEnabled 判断,如:

if(log.isDebugEnabled()){
    // 如果日志不反对参数化模式,debug 又没开启,那字符串拼接就是无用的代码拼接,影响零碎性能
    log.debug("Save order with order no:" + orderNo + ", and order amount:" + orderAmount);
}

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

这里举荐大家用 SLF4J 的门面接口,能够用参数化模式输入日志,debug 级别也不用用 if 判断,简化代码。

4.3 日志的根本格局

日志输入次要在文件中,应包含以下内容:

  • 日志工夫
  • 日志级别次要应用
  • 调用链标识(可选)
  • 线程名称
  • 日志记录器名称
  • 日志内容
  • 异样堆栈(不肯定有)
11:44:44.827 WARN [93ef3E0120160803114444] [main] [ClassPathXmlApplicationContext] Exception encountered during context initialization - cancelling refresh attempt

4.3.1 日志工夫

作为日志产生的日期和工夫,这个数据十分重要,个别准确到毫秒。因为线上个别配置为按天滚动日志文件,日期标识在文件名上,所以能够不放在这个工夫中,应用 HH:mm:ss.SSS 格局即可。非要加上也未尝不可,格局举荐:yyyy-MM-dd HH:mm:ss.SSS

4.3.2 日志级别

日志的输入都是分级别的,不同的设置不同的场合打印不同的日志。上面拿最广泛用的 Log4j 日志框架来做个日志级别的阐明,这个也比拟齐全,其余的日志框架也都大同小异。

次要应用如下的四个级别:

  • DEBUG:DEUBG 级别的次要输入调试性质的内容,该级别日志次要用于在开发、测试阶段输入。该级别的日志应尽可能地详尽,开发人员能够将各类详细信息记录到 DEBUG 里,起到调试的作用,包含参数信息,调试细节信息,返回值信息等等,便于在开发、测试阶段呈现问题或者异样时,对其进行剖析。
  • INFO:INFO 日志次要记录零碎要害信息,旨在保留零碎失常工作期间要害运行指标,开发人员能够将初始化系统配置、业务状态变动信息,或者用户业务流程中的外围解决记录到 INFO 日志中,不便日常运维工作以及谬误回溯时上下文场景复现。倡议在我的项目实现后,在测试环境将日志级别调成 INFO,而后通过 INFO 级别的信息看看是否能理解这个利用的使用状况,如果呈现问题后是否这些日志是否提供有用的排查问题的信息。
  • WARN:WARN 级别的次要输入正告性质的内容,这些内容是能够预知且是有布局的,比方,某个办法入参为空或者该参数的值不满足运行该办法的条件时。在 WARN 级别的时应输入较为详尽的信息,以便于预先对日志进行剖析
  • ERROR:ERROR 级别次要针对于一些不可预知的信息,诸如:谬误、异样等,比方,在 catch 块中抓获的网络通信、数据库连贯等异样,若异样对系统的整个流程影响不大,能够应用 WARN 级别日志输入。在输入 ERROR 级别的日志时,尽量多地输入办法入参数、办法执行过程中产生的对象等数据,在带有谬误、异样对象的数据时,须要将该对象一并输入

4.3.2.1 INFO 和 DEBUG 的抉择

DEBUG 级别比 INFO 低,蕴含调试时更具体的理解零碎运行状态的货色,比方变量的值等等,都能够输入到 DEBUG 日志里。INFO 是在线日志默认的输入级别,反馈系统的以后状态给最终用户看的。输入的信息,应该对最终用户具备实际意义的。从性能角度上说,Info 输入的信息能够看作是软件产品的一部分,所以须要审慎看待,不可轻易输入。尝试记录 INFO 日志时无妨在头脑中模仿线上运行,如果这条日志会被频繁打印或者大部分工夫对于纠错起不到作用,就该当思考下调为 DEBUG 级别。

  • 因为 info 及 debug 日志打印量远大于 ERROR,出于前文日志性能的思考,如果代码为外围代码,执行频率十分高,务必斟酌日志设计是否正当,是否须要下调为 DEBUG 级别日志。
  • 留神日志的可读性,无妨在写完代码 review 这条日志是否通顺,是否提供真正有意义的信息。
  • 日志输入是多线程专用的,如果有另外一个线程正在输入日志,下面的记录就会被打断,最终显示输入和料想的就会不统一。

4.3.2.2 WARN,ERROR 的抉择

当办法或者性能处理过程中产生不合乎预期后果或者有框架报错时能够思考应用,常见问题解决办法包含:

  • 减少判断解决逻辑,尝试本地解决:减少逻辑判断吞掉报警永远是最优抉择。
  • 抛出异样,交给下层逻辑解决
  • 记录日志,报警揭示
  • 应用返回码包装谬误做返回

一般来说,WARN 级别不会短信报警,ERROR 级别则会短信报警甚至电话报警,ERROR 级别的日志意味着零碎中产生了十分重大的问题,必须有人马上解决,比方数据库不可用,零碎的要害业务流程走不上来等等。谬误的应用反而带来重大的结果,不辨别问题的重要水平,只有有问题就 error 记录下来,其实这样是十分不负责任的,因为对于成熟的零碎,都会有一套残缺的报错机制,那这个错误信息什么时候须要收回来,很多都是根据单位工夫内 ERROR 日志的数量来确定的。因而如果咱们不分轻重缓急,一律 ERROR 看待,就会徒增报错的频率,长此以往,咱们的救火队员对谬误警报就不会那么在意,这个警报也就失去了原始的意义。

WARN 代表可复原的异样,此次失败不影响下次业务的执行,开发人员会苦恼某些场景下几次失败可容忍,频率高的时候须要揭示,记录 ERROR 的后果是线上时不时呈现容忍范畴内的报警,这时报警是无意义的。但反之不记录 ERROR 日志,真正呈现问题则不会有实时报警,错过最佳解决机会。

强调 ERROR 报警

  • ERROR 级别的日志打印通常随同报警告诉。ERROR 的报出应该随同着业务性能受损,即下面提到的零碎中产生了十分重大的问题,必须有人马上解决。

ERROR 日志指标

  • 给解决者间接精确的信息:error 信息造成本身闭环。

问题定位:

  • 产生了什么问题,哪些性能受到影响
  • 获取帮忙信息:间接帮忙信息或帮忙信息的存储地位
  • 通过报警晓得解决方案或者找何人解决

日志模板

log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [Probably need to do] [params] .”);
log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [please contact xxx@xxx] [params] .”);

4.3.3 调用链标识

在分布式应用中,用户的一个申请会调用若干个服务实现,这些服务可能还是嵌套调用的,因而实现一个申请的日志并不在一个利用的日志文件,而是扩散在不同服务器上不同利用节点的日志文件中。该标识是为了串联一个申请在整个零碎中的调用日志。

调用链标识格局:

  • 惟一字符串(trace ID)
  • 调用层级(span ID)

调用链标识作为可选项,无该数据时只输入 [] 即可。

4.3.4 线程名称

输入该日志的线程名称,个别在一个利用中一个同步申请由同一线程实现,输入线程名称能够在各个申请产生的日志中进行分类,便于分清以后申请上下文的日志。

4.3.5 日志记录器名称

日志记录器名称个别应用类名,日志文件中能够输入简略的类名即可,看理论状况是否须要应用包名和行号等信息。次要用于看到日志后到哪个类中去找这个日志输入,便于定位问题所在。

4.3.6 日志内容

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

// 举荐
log.debug("Load student(id={}), student: {}" , id , student);

对于一些肯定须要进行拼接字符串,或者须要消耗工夫、节约内存能力产生的日志内容作为日志输入时,应应用 log.isXxxxxEnable() 进行判断后再进行拼接解决,比方:

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 );
}

4.3.7 异样堆栈

异样堆栈个别会呈现在 ERROR 或者 WARN 级别的日志中,异样堆栈含有办法调用链的零碎,以及异样产生的本源。异样堆栈的日志属于上一行日志的,在日志收集时须要将其划至上一行中。

4.4 日志文件

日志文件搁置于固定的目录中,依照肯定的模板进行命名,举荐的日志文件名称:

以后正在写入的日志文件名:< 利用名 >[-< 性能名 >].log
曾经滚入历史的日志文件名:< 利用名 >[-< 性能名 >].log.<yyyy-MM-dd>

4.5 日志配置

依据不同的环境配置不同的日志输入形式:

  • 本地调试能够将日志输入到管制台上
  • 测试环境或者生产环境输入到文件中,每天产生一个文件,如果日志量宏大能够每个小时产生一个日志文件
  • 生产环境中的文件输入,能够思考应用异步文件输入,该种形式日志并不会马上刷新到文件中去,会产生日志延时,在进行利用时可能会导致一些还在内存中的日志未能及时刷新到文件中去而产生失落,如果对于利用的要求并不是十分高的话,可暂不思考异步日志

logback 日志工具能够在日志文件滚动后将前一文件进行压缩,以缩小磁盘空间占用,若应用 logback 对于日志量宏大的利用倡议开启该性能。

4.6 日志应用标准

  1. 在一个对象中通常只应用一个 Logger 对象,Logger 应该是 static final 的,只有在多数须要在构造函数中传递 logger 的状况下才应用 private final。
private static final Logger log = LoggerFactory.getLogger(Main.class);
  1. 不要应用具体的日志实现类
InterfaceImpl interface = new InterfaceImpl();

这段代码大家都看得懂吧?应该面向接口的对象编程,而不是面向实现,这也是软件设计模式的准则,正确的做法应该是。

Interface interface = new InterfaceImpl();

日志框架外面也是如此,下面也说了,日志有门面接口,有具体实现的实现框架,所以大家不要面向实现编程。

  1. 输入 Exceptions 的全副 Throwable 信息。因为 log.error(msg)log.error(msg,e.getMessage())这样的日志输入办法会失落掉最重要的 StackTrace 信息。
void foo(){
    try{//do somehing}catch(Exception e){log.error(e.getMessage());// 谬误示范
        log.erroe("Bad Things",e.getMessage());// 谬误示范
        log.error("Bad Things",e);// 正确演示
    }
}
  1. 不容许记录日志后又抛出异样。如捕捉异样后又抛出了自定义业务异样,此时无需记录谬误日志,由最终捕捉方进行异样解决。不能又抛出异样,又打印谬误日志,不然会造成反复输入日志。
void foo() throws LogException{
    try{//do somehing}catch(Exception e){log.error("Bad Things",e);// 正确
        throw new LogException("Bad Things",e);
    }
}
  1. 不容许应用规范输入

包含 System.out.println()System.error.println()语句。因为这个只会打印到控制台,而不会记录到日志文件中,不方便管理日志。此外,规范输入不会显示类名和行号信息,一旦代码中大量呈现规范输入的代码,且日志中打印有规范输入的内容,很难定位日志内容和日志打印的地位,根本无法排查问题,想删除无用日志输入也改不动,这个是笔者在重构古董代码的时候亲自踩过的一个坑。

void foo(){
    try{//do somehing}catch(Exception e){Syste.out.println(e.getMessage());// 谬误
        System.error.println(e.getMessage());// 谬误
        log.error("Bad Things",e);// 正确
    }
}
  1. 不容许呈现 printStackTrace
void foo(){
    try{//do somehing}catch(Exception e){e.printStacktrace();// 谬误
        log.error("Bad Things",e);// 正确
    }
}

来看一下它的源码:

public void printStackTrace() {printStackTrace(System.err);
}

它其实也是利用 System.err 输入到了 Tomcat 控制台。

  1. 禁止在线上环境开启 debug 级别日志输入

出于日志性能的思考,如果代码为外围代码,执行频率十分高,则输入日志倡议减少判断,尤其是低级别的输入 <debug、info、warn>。

一是因为我的项目自身 debug 日志太多,二是各种框架中也大量应用 debug 的日志,线上开启 debug 不久就会打满磁盘,影响业务零碎的失常运行。

  1. 不要在大循环中打印日志

如果你的框架应用了性能不高的 Log4j 框架,那就不要在上千个 for 循环中打印日志,这样可能会拖垮你的应用程序,如果你的程序响应工夫变慢,那要思考是不是日志打印的过多了。

for(int i=0; i<2000; i++){log.info("XX");
}

最好的方法是在循环中记录要点,在循环里面总结打印进去。

  1. 打印有意义的日志

通常状况下在程序日志里记录一些比拟有意义的状态数据:程序启动,退出的工夫点;程序运行耗费工夫;耗时程序的执行进度;重要变量的状态变动。

五、参考资料

  1. Java 程序如何正确地打日志
  2. Java 利用中的日志
  3. 优良日志实际准则
  4. Java 罕用日志框架介绍

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)

2. 别在再满屏的 if/ else 了,试试策略模式,真香!!

3. 卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅公布,光明模式太炸了!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0