• Home
  • About
    • wanziの遇笺 photo

      wanziの遇笺

      一点随笔,一丝感悟,一些记录,一种成长。

    • Learn More
    • Instagram
    • Github
  • Archive
  • Category
  • Tag

Java日志管理

17 Aug 2017

Reading time ~1 minute

一、前言

  日志是所有软件系统非常重要的一部分;良好统一的日志规范和严格执行会大大提高系统的可维护性、可用性、可靠性,并进而提高开发效率, 指导业务。

  • 过程监控--通过分析程序的执行过程,用于验证程序的执行是否按照既定的方式运行。
  • 问题定位--通过查看程序错误日志的详细信息以及产生位置,迅速定位问题产生的原因。
  • 业务指导,通过统计和分析相关的业务、用户行为日志,对业务进行预测指导。
  • 数据恢复--通过反向执行过程日志,可以将数据可以会滚到之前的状态。
  • 健康检查以及系统优化--通过查看系统日志,确定程序运行的健康状况,调节系统参数,优化程序性能。

二、背景

  猛地发现自己一直错误地把Sl4j和java.util.logging、Logback、Log4j理解为同一层次的概念。 继而查了相关的资料,站在更高一层,理解一下Java的日志管理相关的框架————Log4j、JUL、JCL、Slf4j、Logback 、Log4j2。

  分为两个层次的概念:

  • 日志框架(Implement):日志系统是日志的具体实现。如Log4j、java.util.Logging、Logback、Log4j2。
  • 日志门面(Facade):为了解决多个日志系统的兼容问题,日志门面应运而生。如Commons logging和Slf4j。

三、Java常见日志库的历史

  Logging frameworks出现之前(Java 1.3及以前),Java打日志依赖System.out.println(),System.err.println()或者 e.printStackTrace()。Debug日志被写入STDOUT流,错误日志被写入STDERR流。

  Logging frameworks的出现: 先来一幅图从整体上看一下Java常见日志库的时间先后顺序:

1. Log4j

  Log4j可以说是一个里程碑式的框架,它提出的一些基本理念,深深地影响了后来者,直至今天,这些理念也依然在被广泛使用:

  • Logger--Logger负责捕捉事件并将其发送给合适的Appender。
  • Appender--也被称为Handler,负责将日志事件记录到目标位置。在将日志事件输出之前,Appenders使用Layouts来对事件进行格式化处理。
  • Layout--也被称为Formatter,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式。

2. Java Util Log

  Sun公司开始意识到JDK需要一个记录日志的特性。受Log4j的启发,Sun在Java 1.4版本中引入了一个新的API, 叫java.util.logging, 但是,JUL功能远不如Log4j完善,如果开发者要使用它,就意味着需要自己写Appenders(Sun称它为Handlers),而且,只有两个Handlers 可被使用:Console和File,这就意味着,开发者只能将日志写入Console和文件。

如前面所述,JUL在Java 1.4才被引入,在这之前,并没有官方的日志库供开发者使用。于是便有了很多日志相关的”轮子”。我想这应该是当前 会有如此多日志框架的一个很重要的原因。

3. Commons Logging

  由于项目的日志打印必然依赖以上两个框架中至少一个,无论是jul还是Log4j,开发者必须去两个都配置。这时候,Apache的 Commons logging(Jakarta Commons Logging)出现了。
  它的主要作用是提供一个日志门面,使用者可以使用不同的日志实现。用户可以自由选择第三方的日志组件作为具体实现(Log4j或JUL), JCL会通过动态查找的机制,在程序运行时自动找出真正使用的日志库。JCL内部有一个Simple logger的简单实现,但是功能很弱。

4. Slf4j

  JCL是在运行期间查找符合条件的日志类,然后使用类加载的方式加载指定的日志类。这种机制带来了不可预见的后果,如类加载问题, 这会让开发者遇到各种奇怪的问题,增加了调试的困难度。 也正是因为如此,Log4j 的作者发起了另一个项目,也就是 Slf4j(Simple Logging Facade for JAVA)。
  Slf4j 同 JCL类似,也是提供了一个公共的接口,开发者只需要关注接口,不用关心下层实现。同时 Slf4j 的实现只要遵循这个接口, 就可以做到各个日志系统之间的无缝兼容。

5. Logback

  Logback是Slf4j接口的一套具体实现,又是同一个作者,因而保证了其和Log4j相近的使用方式,也具有Slf4j的全部特性。

  现在我们有了两个流行的 Log Facade,以及三个流行的 Log Implementation。Gülcü 是个追求完美的人,他决定让这些Log之间 都能够方便的互相替换,所以做了各种 Adapter 和 Bridge 来连接。

在这里需要注意不能搞出循环的桥接,比如下面这些依赖就不能同时存在:

  • jcl-over-slf4j 和 slf4j-jcl
  • log4j-over-slf4j 和 slf4j-log4j12
  • jul-to-slf4j 和 slf4j-jdk14

6. Log4j2

  Log4j2 是 Log4j、Slf4j、Logback 的作者的又一个作品,它对 Log4j 做了很大的改进,提供了多种现代特性,而且它的异步性能优越, 在分布式系统中性能要远好于 Logback,同时也支持 Slf4j 与 JCL。
  Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。

  现在好了,我们有了三个流行的Log Facade和四个流行的Log Implementation,这时候桥接关系的图如下图:

四、Java 日志框架对比

  这里主要讲述一下几个日志框架(Log4j、JUL、Logback、Log4j2)的对比。

1. Log4j VS JUL

  • 处理器

  JUL包含4种具体的handler的实现,而Log4j则包括超过12个的appender实现。
  JUL的handler足够用来进行基本的日志记录 - 他们允许你写入到一个buffer,一个console,一个socket,和一个file中。 Log4j的appenders,另一方面,大概覆盖了所有logging输出目的地你可以想到的。他们可以写到NT日志或者Unix syslog中,或者甚至发送Email。

  • 格式化器

  JUL包含了两个格式化类:XMLFormatter和SimpleFormatter。Log4j包含了对应的布局器:XMLLayout和SimpleLayout.Log4j还提供了 TTCCLayout,它格式化LoggingEvents到富内容字符串,和HTMLLayout,它可格式化LoggingEvent到HMTL表格中。

2. Logback VS Log4j

  Logback当前分成三个模块:logback-core,logback- classic和logback-access:

  • logback-core是其它两个模块的基础模块。
  • logback-classic是log4j的一个改良版本。此外logback-classic完整实现Slf4j API使你可以很方便地更换成其它日记系统如Log4j或 JDK14 Logging。
  • logback-access访问模块与Servlet容器集成提供通过Http来访问日记的功能。

3. Log4j2 VS Logback

Log4j2是Log4j的升级版,与之前的版本Log4j 1.x相比、有重大的改进,在修正了Logback固有的架构问题的同时,改进了许多 Logback所具有的功能。log4j2与log4j1发生了很大的变化,不兼容。

  • 性能

  由于采用了更先进的锁机制和LMAX Disruptor库,Log4j2的性能优于Logback,特别是在多线程环境下和使用异步日志的环境下。

  • 垃圾

  Log4j2实现了”无垃圾”和”低垃圾”模式。Log4j2在记录日志时,能够重用对象(如String等),尽可能避免实例化新的临时对象, 减少因日志记录产生的垃圾对象,减少垃圾回收带来的性能下降。
  Logback能够自动压缩/删除旧日志。

  • 与Slf4j的适配

  二者都能够适配Slf4j,Logback与Slf4j的适配应该会更好一些,毕竟省掉了一层适配库

五、Java 日志门面对比

  这里主要对比一下日志门面Commons logging和Slf4j:

  Common logging通过动态查找的机制,在程序运行时自动找出真正使用的日志库。由于它使用了ClassLoader寻找和载入底层的日志库, 导致了象OSGI这样的框架无法正常工作,因为OSGI的不同的插件使用自己的ClassLoader。 OSGI的这种机制保证了插件互相独立, 然而却使Apache Common-Logging无法工作。

  Slf4j在编译时静态绑定真正的Log库,因此可以在OSGI中使用。另外,Slf4j 支持参数化的log字符串,避免了之前为了减少字符串拼接 的性能损耗而不得不写的if(logger.isDebugEnable()),现在你可以直接写:logger.debug(“current user is: {}”, user)。 拼装消息被推迟到了它能够确定是不是要显示这条消息的时候,但是获取参数的代价并没有幸免。

六、日志框架与日志门面组合

  使用日志门面可以方便的切换具体的日志实现。而且如果依赖多个项目,使用了不同的Log Facade,还可以方便的通过Adapter 转接到同一个实现上。 因此为了考虑扩展性,一般我们在程序开发的时候,会选择使用commons-logging或者slf4j这些日志门面, 而不是直接使用log4j或者logback这些实现。

  以下是几种比较常见的日志方案组合:

1. JCL + Log4j

  需要的jar包:

  • commons-logging
  • log4j

2. Slf4j + Logback

  需要的jar包:

  • slf4j-api
  • logback-core
  • logback-classic(集成包)

3. Log4j2

  Log4j2需要的jar分成2个:

  • log4j-api: 作为日志接口层,用于统一底层日志系统
  • log4j-core : 作为上述日志接口的实现,是一个实际的日志框架

4. JCL + log4j2

  需要的jar包:

  • commons-logging
  • log4j-api
  • log4j-core
  • log4j-jcl(log4j2与commons-logging的集成包)

  现在的方式JCL + Log4j组合在性能和部分功能上都比较弱,如果要改进可以考虑以下几点:

  • 考虑性能和占位符等功能方面

  推荐slf4j + logback方式 或者log4j2方式,这种方式对现有系统迁移改动较大,无论是代码内log声明还是配置文件上,而且slf4j不支持fatal打印;

  • 考虑系统迁移性

  推荐commons-logging+log4j2,代码不需要改动,只需要改动对应log4j配置文件即可,但是无法利用其占位符功能;

  • 新系统搭建

  不涉及到系统迁移的情况,新系统搭建可以采用纯log4j2方式,其既提供了接口也提供了实现,在性能上也得到了比较大提升。

七、NDC、MDC、ThreadContext

  当处理多线程应用程序,特别是web服务时,跟踪事件可能会变得困难。当针对多个同时存在的多个用户生成日志记录时, 你如何区分哪个行为和哪个日志事件有关呢?如何两个用户没有成功打开一个相同的文件,或者在同一时间没有成功登陆,那么怎么处理日志记录?你可能需要一种方式来将日志记录和程序中的唯一标示符关联起来,这些标识符可能是用户ID,会话ID或者设备ID。而这就是NDC、MDC以及ThreadContext的用武之地。

  NDC、MDC和ThreadContext通过向单独的日志记录中添加独一无二的数据戳,来创建日志足迹(log trails)。 这些数据戳也被称为鱼标记(fish tagging),我们可以通过一个或者多个独一无二的值来区分日志。这些数据戳在每个线程级别上进行管理,并且一直持续到线程结束,或者直到数据戳被删掉。例如,如果你的Web应用程序为每个用户生成一个新的线程,那么你可以使用这个用户的ID来标记日志记录。当你想在一个复杂的系统中跟踪特定的请求、事务或者用户,这是一种非常有用的方法。

1. NDC

  NDC或者嵌套诊断上下文(Nested Diagnostic Context)是基于栈的思想,信息可以被放到栈上或者从栈中移除。 而栈中的值可以被Logger访问,并且Logger无需显示想日志方法中传入任何值。

  • NDC.push()方法将值存储在栈中;
  • NDC.pop()方法将一些项从栈中移除;
  • NDC.remove()方法让Java回收内存,以免造成内存溢出。

2. MDC

  MDC或者映射诊断上下文和NDC很相似,不同之处在于MDC将值存储在键值对中,而不是栈中。这样你可以很容易的在Layout中引用一个单独的键。

  • MDC.put(key,value) 方法将一个新的键值对添加到上下文中;
  • MDC.remove(key) 方法会移除指定的键值对;
  • MDC.clear()方法将所有的键值对从MDC中移除,这样会降低内存的使用量,并阻止MDC在后面试图调用那些已经过期的数据。

Slf4j 只有 MDC,没有 NDC Logback内置没有实现NDC,但是slf4j-ext包提供了一个NDC实现,它使用MDC作为基础。

3. ThreadContext

Log4j版本2中将MDC和NDC合并到一个单独的组件中,这个组件被称为ThreadContext(线程上下文)。

  ThreadContext可以看成是NDC和MDC的结合体,它分别用Thread Context Stack和Thread Context Map来表示NDC和MDC。

  • ThreadContext.clearStack(),清除NDC;
  • ThreadContext.clearMap(),清除MDC;
  • ThreadContext.clearAll(),清除所有。

八、总结

  本文从日志的好处讲起,先总体上概述了日志相关库分为:日志框架和日志门面;然后按照时间顺序列举了这些日志常见库; 随后对它们进行简单的对比;提出了几个常见的日之框架与日志门面的组合使用; 最后提到了NDC、MDC、ThreadContext的概念为下一篇文章提供一点理论基础。
  针对每个库更详细的特性或者具体的使用步骤,大家可以自行去对应的官网查看。

九、Reference

  • Java 日志系统梳理
  • 聊一聊那些年我们用过的 Java 日志框架
  • Java 日志管理最佳实践
  • Java日志组件介绍
  • Log4j 对比 java.util.logging
  • Log4J与Java Logging调研
  • 日志工具现状调研
  • 在Web 应用中增加用户跟踪功能


javalogging Share Tweet +1