Fork me on GitHub

Java中日志体系整理

目录

  • 背景
  • 第一部分 日志发展历程
  • 第二部分 日志分类
  • 第三部分 最佳实践
  • 参考文献及资料

背景

对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。

第一部分 日志发展历程

1.1 第一阶段

2001年前,Java还没有日志库,通过System.outSystem.err打印和记录日志。这个方式通常作为开发调试时候使用,线上生产环境使用主要缺点有:打印过程有大量IO、输出只能在控制台打印无法持久化保存在文件。

该阶段还未有成熟的日志框架(Logging Framework )。

1.2 第二阶段

2001 年,瑞士工程师ceki Gulcü的研发了一个日志框架 log4j(后来将log4j 项目捐献给Apache,成为 Apache 项目,Ceki 加入 Apache 组织)。

log4j 的前生是SEMPER(欧洲安全电子市场项目组)日志工具: Tracing API 。后通过迭代优化后,在IBM 公共许可证下开源。参考Ceki Gülcü博客文章:Log4j delivers control over logging

ceki GulcüJava 日志发展中关键人物,整个Java的日志体系几乎都有他参与或者受到了ceki Gulcü的影响。

再后来Log4j捐献给Apache基金会,ceki Gulcü也一同”嫁入”ApacheLog4j逐渐成为Java社区的日志标准。还有一种说法,Apache基金会曾经建议Sun引入Log4jJava的标准库中,但是被拒绝了。原因是Sun是有’’私心”的,因为他想搞自己的日志标准库。

已经出现第一个日志框架(Logging Framework ):Log4j

1.3 第三阶段

2002年2月JDK 1.4正式发布,Sun终于拿出了自己的日志标准库JUL(Java Util Logging)。其实是”借鉴” Log4j 实现的,而且直到 JDK1.5 后,性能和可用性才有了提升。JUL 出来晚,还不如Log4j好用,程序员们的眼光是雪亮的,所以Log4j仍然比JUL使用广泛。

虽然Log4jJUL好用,但是JUL是原生在JDK中的,根正苗红,仍然有一部分用户。作为用户研发,可以任选一个日志架构。但是如果在一个项目中应用程序和第三方库使用不同日志框架呢?标准不统一影响整个软件业的生产力。

出现第二个日志框架(Logging Framework ):JUL(Java Util Logging)。日志框架出现竞争。

1.4 第四阶段

紧接着,同年(2002年)8月Apache又推出JCL(Jakarta Commons Logging)JCL其实只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现。也就是说,在你应用代码里,只需调用JCL的接口,底层实现可以是Log4j,也可以是Sun的标准库JUL

Apache的心思就是想通过增加一层Interface,然后统一两种日志框架。

注意这各阶段出现了第一个日志门面(Logging Interface):JCL(Jakarta Commons Logging)

JCL

1.5 第五阶段

2006 年 ceki Gulcü 离开 Apache 基金会(原因参考他自己的博客:The forces and vulnerabilities of the Apache model),自立门户,然后就搞了一套新的日志标准接口规范 Slf4j (Simple Logging Facade for Java)Slf4j对标JCL,比JCL优秀是自然的(Slf4j知道JCL的痛点和不足)。另外为了吸引其他日志框架的用户,还搞了一系列桥接包,帮助 Slf4j 与其他日志框架、日志门面的对接,称为桥接设计模式(Adapter)。

  • JCL (日志门面)与 Slf4j (日志门面)之间的桥接包是 jcl-over-slf4j.jar;
  • Log4j(日志框架)与 Slf4j (日志门面)之间的桥接包是 slf4j-log4j12.jar;

详细如下图:

Slf4j_1

这样项目代码使用 Slf4j 接口,可以实现日志的统一标准化。以后如果想改变日志实现,只需引入对应的 Slf4j 桥接包和具体的日志标准库就可以了。

但是还有这样的场景:当前项目日志框架和日志门面为:Log4j+Slf4j,但是后续项目计划使用日志门面JCL,这时候难道要拔掉Slf4j?没事,ceki Gulcü又搞出了新的桥接:slf4j-jcl.jar,如下图。

桥接( Bridging legacy)劫持所以第三方日志输出并重定向至 SLF4j,最终实现统一日志上层 API(编码) 与下层实现(输出日志位置、格式统一)

Slf4j

上面的图中还有多个连接(含箭头方向)没有补完整。后续如图又补充下面的桥接。

  • JUL (日志框架)与 Slf4j (日志门面)之间的桥接包是 jul-to-slf4j.jarslf4j-jdk14.jar;
  • Log4j(日志框架)与 Slf4j (日志门面)之间的桥接包是 log4j-over-slf4j.jar;

最终Slf4j一统江湖:

Slf4j_2

1.6 第六阶段

发展到这个阶段(如上图),日志生态已经很复杂了。但是ceki Gulcü 认为最初他实现是日志框架Log4j存在不足想进一步优化,但是现状是Log4jApache手上。所以只能另起炉灶创建新的日志框架: Logback,并且作为 Slf4j 日志门面的默认实现。Logback在功能完整性和性能上超越了所有现有的日志框架。

Logback作为日志门面Sil4j的默认日志框架,无需额外的日志桥接。如下图:

Logback

LogbackSil4j结合使用,避免复杂的桥接包和桥接关系。新项目考虑直接以这种方式实现,减少日志的维护成本,并且具有性能优势。事情在向着好的方向发展。

1.7 第七阶段

但是Apache并不会放弃竞争。2012年,Apache也推出新的日志项目Log4j2(不兼容Log4j,相当于重写),Log4j2借鉴 Slf4j+LogbackLog4j2分为两个部分:

  • log4j-core.jar,属于日志框架(Logging Framework);
  • log4j-api,属于日志门面(Logging Interface);

Log4j2

Log4j2性能有提高,支持异步日志打印等。但是由于和原来Log4j日志框架(Log4j 1.x)不兼容,仍然需要通过大量的日志桥接来互相兼容。

1.8 小结

整体上看,整个Java日志生态的发展都是 ceki GulcüApache基金会的竞争。目前Java日志生态,对于新手这是复杂的。在日常使用过程中,如果没有了解背后的体系,使用门框较高,还容易出现各种问题。但目前这个状态已经是既定现实。

本节我们按照时间线梳理了日志生态的形成和发展过程。接下来我们会按照功能,介绍目前日志生态的细节。

第一部分 日志依赖分类

2.1 日志框架(Logging Framework

从上面章节的介绍,日志框架是日志生态中底层日志实现类。用来控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等。也可以控制每一条日志的输出格式。通过定义每一条日志信息的级别(七种日志级别:OFF、FATAL、ERROR、WARN、INFO、DEBUG和TRACE),更加细致地控制日志的生成过程。

按照时间顺序,常见日志框架有4种:

  • Log4j

    log4jJava日志框架的元老。1999年发布首个版本,2012年发布最后一个版本,2015年正式宣布终止。至今还有还有很多项目在使用log4j

  • JUL(jdk-logging)

    Sun公司出品,JDK自带原生日志框架。

  • Logback

    作为日志门面Sil4j的默认日志框架,性能优异。

  • Log4j2

    Apache基金会出品,Log4j的继任者。

2.2 日志门面(Logging Interface

日志门面的出现是为了解决J.U.L(jdk-logging)Log4j两个日志框架统一的问题。日志门面作为应用程序和日志关键之间的中间件层(日志框架和应用程序的桥梁作用)。解耦之后,应用程序可以在不修改源代码,只需要调整日志框架依赖包和配置,就可以更换日志框架。这就需要日志门面有强大的兼容多日志框架的兼容能力。

计算机学科一句哲理:“Any problem in computer science can be solved by another layer of indirection.” “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”.

按照时间顺序,常见的日志门面有:

  • JCL(Apache Commons Logging)

    Apache基金会出品,最早的日志门面。原名:Jakarta Commons Logging(所以简称还是JCL)。

  • Slf4j

    Ceki Gülcü出品。

  • Log4j2log4j-api

    Apache基金会出品。

2.3 桥接类

多种日志实现框架混用情况下,需要借助桥接类进行日志的转换,最后统一成一种进行输出。

  • slf4j-jdk14
  • slf4j-log4j12
  • log4j-slf4j-impl
  • logback-classic
  • slf4j-jcl
  • jul-to-slf4j
  • log4j-over-slf4j
  • icl-over-slf4j
  • log4j-to-slf4j

第三部分 日志配置文件

日志框架 配置文件名
Log4j log4j.properties
JUL(jdk-logging) logging.properties
Logback logback.xml
Log4j2 log4j2.xml

注意:log4j2则已经弃用了.properties方式,采用的是.xml

https://www.cnblogs.com/chanshuyi/p/something_about_java_log_framework.html

3.1 日志配置

常见日志配置文件常用配置项目说明如下:

  • Loggers:被称为记录器,应用程序通过获取Logger对象,调用其API来来发布日志信息。Logger通常时应用程序访问日志系统的入口程序。
  • Appenders:也被称为Handlers,每个Logger都会关联一组Handlers,Logger会将日志交给关联Handlers处理,由Handlers负责将日志做记录。Handlers在此是一个抽象,其具体的实现决定了日志记录的位置可以是控制台、文件、网络上的其他日志服务或操作系统日志等。
  • Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式。
  • Level:每条日志消息都有一个关联的日志级别。该级别粗略指导了日志消息的重要性和紧迫,我可以将Level和Loggers,Appenders做关联以便于我们过滤消息。
  • Filters:过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。

3.2 日志级别

各级别按降序排列如下:

日志级别 说明
SEVERE(最高值) SEVERE 是指示严重失败的消息级别
WARNING WARNING 是指示潜在问题的消息级别
INFO INFO 是报告消息的消息级别
CONFIG CONFIG 是用于静态配置消息的消息级别
FINE FINE 是提供跟踪信息的消息级别
FINER FINEST 指示一条最详细的跟踪消息
FINEST(最低值) FINEST 指示一条最详细的跟踪消息

特殊级别

OFF:可用来关闭日志记录。
ALL:启用所有消息的日志记录。

###

第四部分 最佳实践

3.1 规范

面多日志生态中,这么丰富的选择(4个日志框架、3个日志门面),新的研发项目在选择日志方案的时候,如何抉择呢?当然,作为企业研发规范肯定是需要内部统一。

例如阿里Java开发手册中日志规约中有下面的约定:

【强制】应用中不可直接使用日志系统 (Log4jLogback) 中的 API, 而应依赖使用日志框架 SLF4] 中的 API, 使用门面模式的日志框架, 有利于维护和各个类的日志处理方式统一。

1
2
3
4
5
> import org.slf4j.Logger; 
> import org.slf4j.LoggerFactory;
>
> private static final Logger logger = LoggerFactory.getLogger(Abc.class);
>

规范中,要求研发中我们应该使用日志门面,而不是直接使用日志框架。即依赖日志的抽象,而不是日志的实现。总结如下:

  • 使用日志门面;

  • 使用一个日志框架;

  • 依赖不传递;

了解了日志的发展历史,那现在我们再回过头来看看如果,你的系统在选择日志方案的时候,如何抉择呢?毕竟我们3个日志接口,以及4个日志产品

  • 显然第一点是使用日志接口的API而不是直接使用日志产品的API
    这一条也是必须的,也是符合依赖倒置原则的,我们应该依赖日志的抽象,而不是日志的实现

  • 日志产品的依赖只添加一个
    当然也这个也是必须的,依赖多个日志产品,只会让自己的应用处理日志显得更复杂,不可统一控制

  • 把日志产品的依赖设置为Optionalruntime scope
    其中Optional是为了依赖不会被传递,比如别的人引用你这个jar,就会被迫使用不想用的日志依赖

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
    <optional>true</optional>
    </dependency>

    scope设置为runtime,是可以保证日志的产品的依赖只有在运行时需要,编译时不需要,这样,开发人员就不会在编写代码的过程中使用到日志产品的API

1
2
3
4
5
6
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<scope>runtime</scope>
</dependency>

3.2 SLF4J+Logback

3.2.1 配置

新建项目中pom文件引入下面的依赖包:

1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>

项目会自动导入下面的依赖:

1
2
3
ch.qos.logback:logback-classic:1.2.11
ch.qos.logback:logback-core:1.2.11
org.slf4j:slf4j-api:1.7.32

然后在项目resources目录创建相关日志配置文件(logback.xml)。配置文件详细介绍参考其他文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
</encoder>
</appender>

<logger name="com.test" level="TRACE"/>

<root level="debug">
<appender-ref ref="STDOUT"/>
</root>

</configuration>

程序文件中如下使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestClass {
static final Logger logger = LoggerFactory.getLogger(TestClass.class);
public static void main(String[] args) {
logger.debug("debug");
logger.info("info");
}
}
//输出
//19:25:30.653 [main] DEBUG TestClass - debug
//19:25:30.657 [main] INFO TestClass - info

3.2.2 配置文件加载顺序

logback允许多配置文件,其加载时读取配置文件的顺序如下:

  1. 在classpath查找logback-test.xml(一般classpath为src/test/resources)
  2. 如果该文件不存在,logback尝试寻找logback.groovy
  3. 如果该文件不存在,logback尝试寻找logback.xml
  4. 如果该文件不存在,logback会在META-INF下查找[com.qos.logback.classic.spi.Configurator](http://logback.qos.ch/xref/ch/qos/logback/classic/spi/Configurator.html)接口的实现
  5. 如果依然找不到,则会使用默认的BasicConfigurator,导致日志直接打印到控制台,日志等级为DEBUG,日志的格式为%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

3.3 桥接包使用

log4j -> log4j2
那么就得去掉 log4j 1.x jar,添加log4j-1.2-api.jar,配合 log4j-api-2.x.x.jar 和 log4j-core-2.x.x.jar 即可,依赖如下

log4j-1.2-api.jar
log4j-api-2.x.x.jar
log4j-core-2.x.x.jar
1
2
3
log4j2 -> log4j
这里我们只是演示如何做桥接,实际上当然不建议这么做的。
理清了上面的jar包作用,就会发现,可以通过 log4j2 -> slf4j -> log4j 的方式来实现。

需要的jar包,根据依赖关系分别是:

log4j-api-2.x.x.jar
log4j-to-slf4j.jar
slf4j-api-x.x.x.jar
slf4j-log4j12-x.x.x.jar
log4j-1.2.17.jar
1
2
3
4
5
log4j -> slf4j
将代码中的log4j日志桥接到 slf4j,需要如下jar包

log4j-over-slf4j-x.x.x.jar
slf4j-api-x.x.x.jar
1
2
log4j2 -> slf4j
将代码中的log4j2日志桥接到 slf4j,需要如下jar包

log4j-api-2.x.x.jar
log4j-to-slf4j-2.x.x.jar
slf4j-api-x.x.x.jar
1
2
3
slf4j -> log4j2

将slf4j日志,采用log4j2实现进行输出,需要如下jar包

slf4j-api-x.x.x.jar
log4j-slf4j-impl.jar
log4j-api.jar
log4j-core.jar

3.4 Spring-Boot 实践

第五部分 大数据研发日志实践

5.1 MapReduce

MapReduce默认采用log4j作为日志框架

share/hadoop/mapreduce/lib

1
log4j-1.2.17.jar

5.2 Spark

Spark默认采用log4j作为日志框架(为了和log4j2区别,也有称为log4j1),并且采用${SPARK_HOME}/conf/log4j.properties,通常是通过其中的log4j.properties.template 模板文件创建。

官网说明:https://spark.apache.org/docs/latest/configuration.html#configuring-logging

依赖包在jars目录中,例如Saprk-2.3.2目录中:

1
2
3
4
5
6
7
8
slf4j-api-1.7.16.jar
slf4j-log4j12-1.7.16.jar
apache-log4j-extras-1.2.17.jar
log4j-1.2.17.jar
commons-logging-1.1.3.jar
# 日志桥接包
jcl-over-slf4j-1.7.16.jar
jul-to-slf4j-1.7.16.jar

如果项目中使用Spark默认的log4j1,那么pom文件中无需额外引入日志依赖包。下面的spark-core包中已经有相关依赖了。

1
2
3
4
5
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>1.6.1</version>
</dependency>

已有相关依赖:

1
2
3
4
5
org.slf4j:slf4j-api:1.7.10
org.slf4j:jul-to-slf4j:1.7.10
org.slf4j:jcl-over-slf4j:1.7.10
log4j:log4j:1.2.17
org.slf4j:slf4j-log4j12:1.7.10

代码中直接使用SLF4J日志门面即可。底层默认使用

将spark默认日志log4j替换为logback

1.将jars文件夹下apache-log4j-extras-1.2.17.jar,commons-logging-1.1.3.jar, log4j-1.2.17.jar, slf4j-log4j12-1.7.16.jar

替换成log4j-over-slf4j-1.7.23.jar,logback-access-1.2.1.jar, logback-classic-1.2.1.jar, logback-core-1.2.1.jar。

2.将conf文件夹下的log4j.properties.template通过 log4j.properties Translator 转换成logback.xml即可

运行试例:

Flink 中使用 SLF4J日志门面实现,但是底层日志实现却默认使用Log4j2作为日志框架。链接:https://nightlies.apache.org/flink/flink-docs-master/zh/docs/deployment/advanced/logging/

依赖包位置在lib/

1
2
3
4
5
# flink-1.13.2 案例
log4j-1.2-api-2.12.1.jar
log4j-api-2.12.1.jar
log4j-core-2.12.1.jar
log4j-slf4j-impl-2.12.1.jar

研发项目的日志依赖配置如下,注意scope 参数为provided

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.12.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.12.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.1</version>
<scope>provided</scope>
</dependency>

默认日志配置文件位置:conf/,配置文件有:

  • log4j-cli.properties:Flink 命令行使用(例如 flink run);
  • log4j-session.properties:Flink 命令行在启动基于 Kubernetes/Yarn 的 Session 集群时使用(例如 kubernetes-session.sh/yarn-session.sh);
  • log4j-console.properties:Job-/TaskManagers 在前台模式运行时使用(例如 Kubernetes);
  • log4j.properties: Job-/TaskManagers 默认使用的日志配置。

Log4j 会定期扫描这些文件的变更,并在必要时调整日志记录行为。默认情况下30秒检查一次,监测间隔可以通过 Log4j 配置文件的 monitorInterval 配置项进行设置。

官方说使用 -Dlog4j.configurationFile= 参数可以传递日志文件

第六部分 拾遗

6.1 log4j和log4j2

前文我们知道log4j2Apachelog4j的重新和升级的日志框架,为了区别通常称为log4j2,以示和旧的区别。注意在Maven引用的时候区别(groupId不同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

<!-- log4j2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<!-- log4j-core包中已经引入了log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>

代码中引入区别:

1
2
3
4
5
6
7
8
9
//log4j:
import org.apache.log4j.Logger;
private final Logger logger = Logger.getLogger(Test.class.getName());

//log4j2:
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
private static Logger logger = LogManager.getLogger(Test.class.getName());

###

参考文献及资料

1.slf4j官网,链接:https://www.slf4j.org/manual.html

2.《阿里Java开发手册》,链接:https://developer.aliyun.com/special/tech-java

3、Flink日志使用手册,链接:https://nightlies.apache.org/flink/flink-docs-master/zh/docs/deployment/advanced/logging/

0%