Java 日志体系与最佳实践

345 阅读21分钟

前言

最近系统学习了java日志相关的内容,接下来将自己的学习内容做一个总结,通过本文的学习你将了解如下内容:

  • java日志发展历史
  • SLF4J 和 Log4j2 的关系
  • java日志配置规范
  • java日志最佳实践

java 日志发展历史

阶段一(无日志框架)

jdk1.4 之前,还没有现成的日志框架,打印日志使用 System.out.printlnSystem.err.println

有如下缺点:

产生大量的 IO 操作,在生产环境无法合理的控制日志是否需要输出;

日志打印到标准控制台,输出的内容无法保存到文件,后续不便于查看日志;

无法定制化,日志的粒度不够细;

阶段二(Log4j)

1999年 Ceki Gülcü 发布 ****Log4j (Log for Java) ,它是是基于Java开发的日志框架,并将 Log4j 捐献给了 Apache 软件基金会,使之成为了Apache日志服务的一个子项目,之后大佬加入 apache。Log4j 作为 Apache 基金会的项目,Apache 希望将 Log4j 引入 JDK,不过被 sun 公司拒绝了。

Log4j 第一次提出了日志级别的概念:all>trace>debug>info>warn>error>fatal

阶段三(JUL)

2002年2月 Java1.4 发布,为了对抗 Log4j,Java 模仿 Log4j 开发了自己的日志工具 JUL(Java Util Logging) ,但是此时的 Log4j 已经非常成熟了,大部分应用都使用的是 Log4j。

JUL 中的 Handler 就是模仿的 Log4j 中的 Appender。比如 JUL 中的 java.util.logging.ConsoleHandler和 Log4j 中的 org.apache.logging.log4j.core.appender.ConsoleAppender 功能类似。

阶段四(JCL)

2002年8月Apache推出了日志接口JCL(Jakarta Commons Logging 或 Apache Commons Logging),因为前面出现了Log4j 和 JUL,apache 希望统一日志抽象,通过动态查找的机制,在程序运行时自动找出真正使用的日志库。Commons Logging 定义了一套日志接口,具体实现则由 Log4j 或 JUL 来完成。开发依赖JCL接口,日志实现可以在 log4j和JUL之间切换,在 classpath 中如果找到 log4j 则使用 log4j,如果没有找到 log4j 则使用 jdk自带的JUL。

JCL 的缺点:

  • JCL 动态查找机制的顺序为如下图,只支持下面的日志实现框架

  • 在使用了自定义 class loader 的程序中,使用 JCL 会引发内存泄漏
  • JCL 打印日志性能较低

阶段五(SLF4J)

2005年大佬 Ceki Gülcü 与 Apache 基金会关于 Commons Logging 制定的标准存在分歧,后来 Ceki Gülcü 离开 Apache 并创建了 SLF4J (Simple Logging Facade for Java) 。S LF4J 是一个日志门面,只提供接口,可以支持 Logback、JUL、log4j 等日志实现。并且大佬开发了很多适配器,如下所示:

适配器适配的日志框架适配的日志框架说明
slf4j-log4j12-2.0.1.jarlog4jlog4j 1.2 版的绑定/提供程序,一个广泛使用的日志框架。鉴于 log4j 1.x 已在 2015 年宣布结束生命周期,从 SLF4J 1.7.35 开始,slf4j-log4j模块在构建时自动重定向到 slf4j-reload4j模块。 假设您希望继续使用 log4j 1.x 框架,我们强烈建议使用 slf4j-reload4j 。
slf4j-reload4j-2.0.1.jarreload4jreload4j 框架的绑定/提供程序 。Reload4j 是 log4j 版本 1.2.7 的直接替代品。你还需要将reload4j.jar放在你的类路径上。
slf4j-jdk14-2.0.1.jarJULjava.util.logging 的绑定/提供程序,也称为 JDK 1.4 日志记录
slf4j-nop-2.0.1.jarNOPNOP的绑定/提供者,默默地丢弃所有日志记录。
slf4j-simple-2.0.1.jarslf4j simple简单 实现的绑定/提供程序,它将所有事件输出到 System.err。只打印级别 INFO 和更高级别的消息。此绑定在小型应用程序的上下文中可能很有用。
slf4j-jcl-2.0.1.jarJCLJakarta Commons Logging的绑定/提供者。此绑定会将所有 SLF4J 日志记录委托给 JCL。

阶段六(Logback)

2006年,ceki 大佬觉得市面上的日志库都是间接实现了 SLF4J 接口,也就是说每次都需要引入适配器包,因此大佬基于 SLF4J 接口开发了 Logback 日志标准库,做为 SLF4J 的默认实现,Logback 十分强大,在功能完整度和性能上超过了当时所有的日志库。Logback 的目的是代替 Log4j,因为Log4j 无法满足高性能的要求。

阶段七(Log4j2)

2012年,由于 Log4j 性能较低,apache 决定重写 Log4j, 2014年正式推出了 Log4j2(不兼容Log4j)。

Log4j2 全面借鉴了 SLF4J + Logback,不仅仅具有 Logback 的所有特性,还做了分离设计,分为 log4j-core.jarlog4j-api.jarlog4j-api 是日志接口,log4j-core是日志标准库,并且 apache 还为 Log4j2 提供了各种适配器。

虽然 Log4j2 有极大的抄袭嫌疑,但是毕竟是最新的日志库,并且吸收 Logback 优秀设计的同时,还解决了很多 Logback 遗留的问题,性能有极大的提升,所以我们选择使用 Log4j2 做为日志库,后续会详细介绍 Log4j2的特性与使用。

总结前面的Java 日志发展历程如下图所示:

目前的Java日志体系如下图:

转存失败,建议直接上传图片文件

前面提到的各个日志框架的代码都可以在GitHub上面找到:

日志门面和日志实现

前面提到的日志框架可以分为两类:日志门面和日志实现

  • 日志门面:只提供日志相关的接口定义,即相应的 API,而不提供具体的接口实现。日志门面在使用时,可以动态或者静态地指定具体的日志框架实现,解除了接口和实现的耦合,使用户可以灵活地选择日志的具体实现框架。
  • 日志实现:与日志门面相对,它提供了具体的日志接口实现,应用程序通过它执行日志打印的功能。

在开发中需要使用日志门面记录日志,而不是具体的日志实现框架,使用日志门面有如下优点:

1、面向接口开发,不再依赖具体的代码实现,减少代码的耦合。

2、 通过导入不同的日志实现依赖,就可以灵活切换日志框架。

3、统一API方便开发者学习和使用。

4、统一配置便于项目的日志管理。

SLF4J相关内容

SLF4J介绍

Java 的简单日志门面 SLF4J (Simple Logging Facade for Java) 用作各种日志框架的简单门面或抽象,例如 java.util.logging(JUL)、logback 和 reload4j(log4j)。SLF4J 允许最终用户在部署时插入所需的日志框架。请注意启用 SLF4J 的库/应用程序意味着仅添加一个强制依赖项,即 slf4j-api-2.0.1.jar

从 1.6.0 开始,如果在类路径上没有找到绑定,那么 SLF4J 将默认为无操作实现 slf4j-nop-2.0.1.jar,会丢弃所有日志。

从 1.7.0 开始,接口中的打印方法 Logger 现在提供接受varargs 而不是Object[]. 此更改意味着 SLF4J 需要 JDK 1.5 或更高版本。在底层 Java 编译器将方法中的可变参数部分转换为 Object[]. 因此,编译器生成的 Logger 接口在 1.7.x 中与其对应的 1.6.x 没有区别。SLF4J 版本 1.7.x 完全与 SLF4J 版本 1.6.x 兼容。

从 1.7.5 开始,记录器检索时间显著改善,鉴于改进的程度,强烈建议用户迁移到 SLF4J 1.7.5 或更高版本。

从 1.7.9 开始,通过将 slf4j.detectLoggerNameMismatch系统属性设置为 true,SLF4J 可以自动发现命名错误的记录器。

从 2.0.0 开始, SLF4J API 2.0.0 版需要 Java 8,并引入了向后兼容的 fluent logging API。通过向后兼容,我们的意思是不必更改现有的日志框架,以便用户从fluent logging API中受益。

从 2.0.0 开始,SLF4J 中的 bindings 被称为 providers 。尽管如此,总体思路还是一样的。SLF4J API 版本 2.0.0 依赖 ServiceLoader 机制来查找其日志记录后端。有关更多详细信息,请参阅相关的常见问题解答条目

了解 service Loader ServiceLoader总结:

ServiceLoader 是JDK提供的一种帮第三方实现者加载服务的便捷方式,如JDBC、日志等,第三方实现者需要遵循约定把具体实现的类名放在/META-INF里。当JDK启动时会去扫描所有jar包里符合约定的类名,再调用forName进行加载,如果JDK的ClassLoader无法加载,就使用当前执行线程的线程上下文类加载器。

但是在通过SPI查找服务的具体实现时,会遍历所有的实现进行实例化,并在循环中找到需要的具体实现。

关于 SLF4J的问题

SLF4J 是另一个日志门面吗?

SLF4J 在概念上与 Jakarta Commons Logging (JCL) 非常相似。因此,它可以被认为是另一个日志门面。然而,SLF4J 在设计上要简单得多,并且可以说更健壮。简而言之,SLF4J 避免了困扰 JCL 的类加载器问题。

如果 SLF4J 修复了 JCL,那么为什么不在 JCL 中进行修复,而是创建一个新项目呢?

首先,SLF4J 的静态绑定方式非常简单,让开发人员相信这种方法的有效性并不容易。请注意,从 2.0.0 版开始,SLF4J 使用 ServiceLoaderJava 平台提供的机制。这种新方法仍然是相对静态的,因此是可预测的。

其次,SLF4J 提供了两个往往被低估的增强功能。参数化日志消息,以实用的方式解决了与日志记录性能相关的一个重要问题。接口支持的标记对象 org.slf4j.Logger为采用高级日志记录系统铺平了道路,并且仍然为在需要时切换回更传统的日志记录系统敞开大门。

到目前为止,这个问题没有实际意义,因为 JCL 已经不复存在了至少十年。

SLF4J的使用

SLF4J 与其他日志框架组合如下图所示,如果要在项目中使用 SLF4J 绑定到相关的日志实现时,只需要导入下图所示的包即可:

为什么要引入适配器,从前面的Java 日志发展可以知道,SLF4J 是日志门面,用来统一日志接口的,日志的实现可以根据需求自由选择,但是有一些日志框架开发的比较早,比如log4j、JUL,没有按照 SLF4J 的接口来实现,所以需要使用适配器转换一下。

比如选择使用 Logback,只需要在 classpath 中引入 logback-classic.jar 即可,不要引入适配器,因为 Logback 是 SLF4J 的原生实现,接口是完全相同的。

比如选择使用 JUL,只需要在 classpath 中引入适配器 slf4j-jdk14.jar,因为 JUL 是 jdk1.4 中自带的,并且JUL 的接口与 SLF4J 的接口不兼容,需要使用适配器转换一下。

这里的内容来自 slf4j 官网,发现一个有意思的事情是,这里并没有提及 log4j2,脑补一下原因,Ceki 大佬开发了 log4j、slf4j、logback(logback 是 slf4j 的原生实现),很早之前大佬把 log4j 贡献给了 apache,自己也加入了apache,后来因为一些原因离开了apache,apache觉得 log4j 性能不好,又重新开发了log4j2,log4j2 模仿的 logback,但是大佬觉得 logback 已经很不错了,使用的人也很多而且 spring boot 默认的日志实现就是 logback,所以就没有添加 log4j2 的相关内容。实际上log4j2 做了很多性能优化,根据 log4j2 的测试,log4j2 的性能高于 logback,所以我们一般选择 log4j2 作为日志框架。

统一混乱的Java日志体系

由于Java日志框架很多,就容易导致混乱。比如项目A使用的 JUL,项目B使用的 Log4j,现在要将两个项目合并,我们需要统一日志框架,但是不想去改代码,或者是第三方依赖我们改不了代码,这时就可以使用桥接器将不同的日志框架桥接到 SLF4J 的日志门面上,然后使用统一的日志实现,不需要维护多套日志配置文件,如下图所示:

下面举一个例子说明上图的情况,比如 log4j-over-slf4j.jar 它实现了 log4j 的公共 API,但底层使用 SLF4J,因此名称为“log4j over slf4j”。

它是如何工作的?

log4j-over-slf4j 模块包含 log4j 使用的类,这些类将所有工作重定向到其相应的 SLF4J 类。要在您自己的应用程序中使用 log4j-over-slf4j,第一步是找到log4j.jar并将其替换为 log4j-over-slf4j.jar。在大多数情况下,从 log4j 迁移到 SLF4J 只需要替换一个 jar 文件。

什么时候不起作用?

当应用程序调用桥接器log4j-over-slf4j 中不存在的 log4j 组件时, 模块将不起作用。例如,当应用程序代码直接引用 log4j 附加程序、过滤器或 PropertyConfigurator 时,log4j-over-slf4j 将不足以替代 log4j。但是,当通过配置文件配置 log4j 时,无论是log4j.properties还是log4j.xml,log4j-over-slf4j 模块应该可以正常工作。

开销呢?

直接使用 log4j-over-slf4j 而不是 log4j 的开销比较小。鉴于 log4j-over-slf4j 立即将所有工作委托给 SLF4J,CPU 开销应该可以忽略不计,大约为几纳秒。每个 logger 的 hashmap 中有一个条目对应的内存开销,即使对于由数千个 logger 组成的非常大的应用程序,这通常也是可以接受的。此外,如果您选择 logback 作为您的底层日志系统,并且考虑到 logback 比 log4j 更快且内存效率更高,则使用 logback 获得的收益应该补偿使用 log4j-over-slf4j 而不是直接使用 log4j 的开销.

log4j-over-slf4j.jar 和 slf4j-reload4j.jar 不能同时存在

slf4j-reload4j.jar的存在,即 SLF4J 的 reload4j 绑定,将强制所有 SLF4J 调用委托给 reload4j,它与 log4j 1.x 具有相同的 API。log4j-over-slf4j.jar的存在将反过来将所有 reload4j API 调用委托给它们的 SLF4J 等效项。如果两者同时存在,SLF4J 调用将被委托给 reload4j(其中包含 log4j 1.x API),而 log4j 调用将被重定向到 SLF4j,从而导致无限循环。

注意:如果日志框架桥接到SLF4J,那么SLF4 J的日志实现不能选择该日志框架,因为会导致无限循环。

比如,log4j 桥接到 SLF4J ,则 SLF4J 不能导入适配 log4j 的jar包,但是可以选择 Logback 或者 JUL。

再举一个例子说明一下上述过程,比如有一个spring 应用使用的日志框架是 JUL,我们的应用程序引入这个应用后需要统一日志框架为 Log4j2,应该如何处理呢?

方案一:

转存失败,建议直接上传图片文件

方案二:

转存失败,建议直接上传图片文件

Log4j2 的相关内容

Log4j 1.x 已被广泛采用并在许多应用程序中使用。然而,多年来,它的发展已经放缓。由于需要与非常旧的 Java 版本兼容,它变得更加难以维护,并 于 2015 年 8 月终止。它的替代品 Logback 进行了许多必要的改进,那么为什么要 Log4j 2 呢?

Log4j2 特性

  1. Log4j 2 旨在用作日志框架。Log4j 1.x 和 Logback 在重新加载配置文件时都会丢失日志事件,Log4j 2 不会。在 Logback 中,Appender 中的异常对应用程序永远是不可见的。在 Log4j 2 中,Appender 可以配置为允许异常渗透到应用程序中。
  2. Log4j 2 包含基于LMAX Disruptor 库的下一代异步记录器。在多线程场景中,异步 Logger 的吞吐量比 Log4j 1.x 和 Logback 高 10 倍,延迟低几个数量级。
  3. Log4j 2对于独立应用程序来说是无垃圾的,在稳定状态日志记录期间对于 Web 应用程序来说是低垃圾的。这减少了垃圾收集器的压力,并且可以提供更好的响应时间性能。
  4. Log4j 2 使用插件系统,通过添加新的AppendersFiltersLayoutsLookups和 Pattern Converters可以非常容易地 扩展框架,而无需对 Log4j 进行任何更改。
  5. 由于插件系统配置更简单,配置中的条目不需要指定类名。
  6. 支持自定义日志级别。自定义日志级别可以在代码或配置中定义。
  7. 支持lambda 表达式。只有在启用了请求的日志级别时,在 Java 8 上运行的客户端代码才能使用 lambda 表达式来延迟构造日志消息。不需要显式级别检查,从而使代码更清晰。
  8. 支持消息对象。消息允许支持有复杂的构造通过日志系统传递并被有效地操作。用户可以自由创建自己的 Message 类型并编写自定义布局过滤器查找来操作它们。
  9. Log4j 1.x 支持 Appenders 上的过滤器。Logback 添加了 TurboFilters 以允许在事件被 Logger 处理之前对其进行过滤。Log4j 2 支持过滤器,这些过滤器可以配置为在事件被 Logger 处理之前处理它们,因为它们是由 Logger 或 Appender 处理的。
  10. 许多 Logback Appender 不接受 Layout,只会以固定格式发送数据。大多数 Log4j 2 Appender 接受一个布局,允许以任何所需的格式传输数据。
  11. Log4j 1.x 和 Logback 中的布局返回一个字符串。这导致了在Logback Encoders中讨论的问题。Log4j 2 采用了更简单的方法,即Layouts总是返回一个字节数组。这样做的好处是这意味着它们几乎可以在任何 Appender 中使用,而不仅仅是写入 OutputStream 的 Appender。
  12. Syslog Appender支持 TCP 和 UDP 以及对 BSD syslog 和RFC 5424格式的支持。
  13. Log4j 2 利用 Java 5 并发支持并在可能的最低级别执行锁定。Log4j 1.x 有已知的死锁问题。其中许多在 Logback 中是固定的,但许多 Logback 类仍然需要相当高的同步。
  14. 它是一个 Apache 软件基金会项目,遵循所有 ASF 项目所使用的社区和支持模型。

Log4j2 安全漏洞

2021 年,Log4j2 爆出了一个极其严重的安全漏洞:

Apache Log4j2 版本 2.0-beta7 到 2.17.0(不包括安全修复版本 2.3.2 和 2.12.4)容易受到远程代码执行 (RCE) 攻击,在这种攻击中,有权修改日志配置文件的攻击者可以构建恶意配置将 JDBC Appender 与引用 JNDI URI 的数据源一起使用,该 JNDI URI 可以执行远程代码。此问题已通过将 JNDI 数据源名称限制为 Log4j2 版本 2.17.1、2.12.4 和 2.3.2 中的 java 协议来解决。

修复建议:

升级到 Log4j 2.3.2(适用于 Java 6)、2.12.4(适用于 Java 7)或 2.17.1(适用于 Java 8 及更高版本)

使用 SLF4J + Log4j2

如果要使用 SLF4J + Log4j2 只需要导入 log4j-slf4j-impl 依赖即可,由于maven 的依赖传递机制,log4j-slf4j-impl内部导入了 slf4j-api、log4j-core、log4j-api,通常情况下我们需要显示定义 slf4j-api 的版本,maven 会选择版本号较大的依赖:

   <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.32</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>2.17.1</version>
    </dependency>

测试代码如下所示:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Log4j2Test {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Main.class);
        logger.trace("trace调用 {} 打印日志", logger.getName());
        logger.debug("debug调用 {} 打印日志", logger.getName());
        logger.info("info调用 {} 打印日志", logger.getName());
        logger.warn("warn调用 {} 打印日志", logger.getName());
        logger.error("error调用 {} 打印日志", logger.getName());
    }
}

输出日志内容如下:

21:15:30.409 [main] ERROR org.demo.Log4j2Test - 调用 SLF4J 接口打印日志

在默认情况下,也就是 classpath 中没有添加 log4j2.xml 配置文件的时,log4j2 的默认日志级别为 ERROR,所以我们的测试代码中需要打印 ERROR 级别的日志才能在控制台输出日志。后面会详细介绍 log4j2 的配置文件。

如果没有引入 log4j-slf4j-impl,运行前面的测试代码 SLF4J 会有如下内容输出:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

由于应用程序中推荐使用 SLF4J 的 API,所以Log4j2 提到的关于 API 的新特性不建议直接使用

应该使用SLF4J日志门面的接口API记录日志,而不应该使用具体的日志实现API记录日志。

Log4j2 配置文件

log4j2自动加载配置文件的顺序

Log4j 能够在初始化期间自动加载配置文件,当 Log4j 启动时,它会找到所有的 ConfigurationFactory 插件,并按从高到低的加权顺序排列它们。

Log4j 支持四种类型的配置文件:

  • properties
  • yaml
  • json
  • xml

Log4j 将检查log4j2.configurationFile系统属性,如果设置,将尝试使用 ConfigurationFactory与文件扩展名匹配的配置加载配置。请注意,这不限于本地文件系统上的位置,可能包含 URL。

1、如果未设置系统属性,则 ConfigurationFactory 将在类路径中查找 log4j2-test.properties。

2、如果未找到前面的文件,则 YAML ConfigurationFactory 将在类路径中查找 log4j2-test.yaml 或 log4j2-test.yml。

3、如果未找到前面的文件,JSON ConfigurationFactory 将在类路径中查找 log4j2-test.json或查找log4j2-test.jsn。

4、如果未找到前面的文件,XML ConfigurationFactory 将 log4j2-test.xml在类路径中查找。

5、如果无法找到测试文件,则 ConfigurationFactory 将 log4j2.properties在类路径中查找 log4j2.properties。

6、如果找不到属性文件,YAML ConfigurationFactory 将在类路径中查找 log4j2.yaml或查找log4j2.yml。

7、如果找不到 YAML 文件,JSON ConfigurationFactory 将在类路径中查找 log4j2.json或查找log4j2.jsn。

8、如果无法找到 JSON 文件,XML ConfigurationFactory 将尝试 log4j2.xml在类路径中定位。

9、如果找不到配置文件,将使用DefaultConfiguration,这将导致日志输出转到控制台。

总结log4j 查找配置文件的顺序为,先查找 log4j2-test 测试文件,然后查找 log4j2 文件,按照文件扩展名排序为:properties > yaml > json > xml

自定义 log4j 配置时,将 log4j2.xml 文件放在 classpath 中即可。

Log4j2 的默认配置

DefaultConfiguration 的配置等同于下面的 log4j2.xml 的配置:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

可以看到,Root 的默认日志级别为 error,并且默认输出日志到控制台。

Log4j2 的配置文件

关于Log4j2 的详细配置可以查看官方文档,log4j2.xml文件的配置大致如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<Configuration>
    <Properties>
    </Properties>
  
    <Appenders>
    </Appenders>

    <Loggers>
    </Loggers>
</Configuration>

配置文件由 PropertiesAppendersLoggers三块组成。

Properties 配置基础变量。

Appenders配置日志收集方式、布局、输出、清理等功能。

Loggers 配置日志级别等。

下面介绍一下常用的配置:

Configuration

Configuration:为根节点,有 status 和 monitorInterval 等多个属性

  • status的值有 trace, debug,info,warn,error,fatal,用于控制log4j2日志框架本身的日志级别,如果将stratus设置为较低的级别就会看到很多关于log4j2本身的日志,如加载log4j2配置文件的路径等信息。
  • monitorInterval 表示每隔多少秒重新读取配置文件,可以不重启应用的情况下修改配置。

一般情况下 Configuration 不需要特殊配置

Properties

Properties: 用来配置基础变量,以便在其他配置的时候引用,该配置是可选的,例如定义日志的存放位置

<Properties>
    <Property name="baseDir">/home/logs</Property>
</Properties>

使用的时候使用 ${baseDir} 引用即可

Appender

Appenders:输出源,用于定义日志输出的地方,具体信息可以查看官方文档。

官方给出了很多的 Appender,如下图所示,比较常用的是 ConsoleRolling File,后面会详细介绍。

ConsoleAppender

ConsoleAppender 将其输出写入 System.out 或 System.err,默认写入 System.out 。必须提供 Layout 来格式化 LogEvent。ConsoleAppender 的参数配置说明如下:

参数名称类型描述
filterFilter一个过滤器,用于确定事件是否应由此 Appender 处理。使用 CompositeFilter 可以使用多个过滤器。
layoutLayout用于格式化 LogEvent 的布局。如果未提供布局,则将使用“%m%n”的默认模式布局。
followboolean标识附加程序是否通过配置后进行的 System.setOut 或 System.setErr 接受对 System.out 或 System.err 的重新分配。请注意,follow 属性不能与 Windows 上的 Jansi 一起使用,不能与 direct 一起使用。
directboolean直接写入java.io.FileDescriptor并绕过java.lang.System.out/.err. 当输出被重定向到文件或其他进程时,可以提供高达 10 倍的性能提升。不能在 Windows 上与 Jansi 一起使用,不能与follow 一起使用, 输出可能与多线程应用程序中的 java.lang.System.setOut()/.setErr()其他输出交织在一起 。自 2.6.2 以来的新功能。请注意,这是一个新增功能,目前仅在 Linux 和 Windows 上使用 Oracle JVM 进行了测试。
nameStringAppender 的名称。
ignoreExceptionsboolean默认为true。设置为true时,如果记录日志发生异常,此条日志和异常将被忽略。设置为false时,异常将被抛出到调用方。如果此append用在FailoverAppender中,则必须设置为false。
targetString只能填写 SYSTEM_OUTSYSTEM_ERR。默认值为 SYSTEM_OUT

ConsoleAppender 一般只需要配置 name 和 target 即可,如下所示:

<Console name="STDOUT-APPENDER" target="SYSTEM_OUT">
    <PatternLayout pattern="%d %-5p %c{2} - %m%n%throwable" charset="UTF-8"/>
</Console>

<Console name="STDERR-APPENDER" target="SYSTEM_ERR">
    <PatternLayout pattern="%d %-5p %c{2} - %m%n%throwable" charset="UTF-8"/>
</Console>

RollingFileAppender

RollingFileAppender 是一个OutputStreamAppender,可以根据TriggeringPolicy和RolloverPolicy将文件切割归档,通过RollingFileManager(扩展了OutputStreamManager)来实际执行文件I / O并执行归档。参数如下:

参数类型描述
appendboolean默认为true。如果为true,记录将附加到文件末尾。设置为false时,将在写入新记录之前清除文件
bufferedIOboolean默认为true。如果为true,数据先写入缓冲区,如果缓冲区满或者immediateFlush 为true时,数据才被写入磁盘,如果为false直接写入磁盘。文件锁定不能与bufferedIO一起使用
bufferSizeint缓冲区大小。bufferedIO为true时,此参数有效,默认为8192 bytes
createOnDemandboolean默认为false。按需创建文件。仅当日志事件通过所有Filter并且路由到该append时,append才创建文件
filterFilter确定事件是否应由此Appender处理,通过CompositeFilter(对应标签为)可以使用多个过滤器
fileNameString要写入的文件名,如果不存在或者父目录不存在,则创建对应的文件或目录
filePatternString归档文件的模式,取决于所使用的RolloverPolicy
immediateFlushboolean默认为true。如果为true,每次写操作后都会将数据刷新入磁盘,可能会影响性能。 每次写入后刷新仅在使用同步appender时才有用。即使设置为false,异步appender也将在一批事件结束后自动刷新,这也可以确保效率更高的将数据写入磁盘。
layoutLayout格式化日志输出格式。如果未设置,则默认为’%m%n’
nameStringappend名称
policyTriggeringPolicy用于确定归档的触发条件,常用 TimeBasedTriggeringPolicy
strategyRolloverStrategy用于确定归档的文件名称、路径及归档方式,常用 DefaultRolloverStrategy
ignoreExceptionsboolean默认为true。设置为true时,如果记录日志发生异常,此条日志和异常将被忽略。设置为false时,异常将被抛出到调用方。如果此append用在FailoverAppender中,则必须设置为false。
filePermissionsString创建文件时指定文件的rwx权限,前提是文件系统应支持POSIX文件属性视图
fileOwnerString文件所有者。出于安全原因,更改文件的所有者可能受到限制,并且不允许操作时会抛出IOException。 如果_POSIX_CHOWN_RESTRICTED对路径有效,则只有有效用户ID等于文件用户ID或具有适当特权的进程才可以更改文件的所有权,前提是文件系统应支持文件所有者属性视图
fileGroupString文件组。,前提是文件系统应支持POSIX文件属性视图

RollingFileAppender 重点需要关注的几个参数是:

  • name
  • fileName
  • filePattern
  • append
  • policy
  • strategy

下面重点介绍一下 policystrategy, policy 决定何时滚动,strategy 决定归档的文件名称。

TriggeringPolicy

TriggeringPolicy 是控制日志文件归档的触发条件。总共有四种类型的 TriggeringPolicy,可以使用 <Policies> 标签组合多种触发策略来控制日志文件归档,如果配置了多种策略,那么只要有一种策略返回true,就返回true。

<Policies>
  <CronTriggeringPolicy schedule="0 0 * * * ?"/>
  <OnStartupTriggeringPolicy minSize="2" />
  <SizeBasedTriggeringPolicy size="20 MB" />
  <TimeBasedTriggeringPolicy />
</Policies>
  • CronTriggeringPolicy

基于 cron 表达式触发日志文件滚动更新,这个策略通过一个 timer 控制,并且异步处理日志事件,因此上一个或下一个时间段的日志事件可能会出现在当前日志文件的开头或结尾。filePattern属性应包含一个时间戳,否则目标文件将在每次归档时被覆盖。参数如下:

参数类型描述
scheduleStringcron表达式,该表达式与Quartz调度程序中允许的表达式相同。详见CronExpression
EvaluationOnStartupboolean启动时,将根据文件的最后修改时间戳评估cron表达式。如果cron表达式指示应该在该时间和当前时间之间归档,则文件将立即被归档。
  • OnStartupTriggeringPolicy

如果日志文件的时间比JVM的启动时间早,或者达到minSize的值,则会触发归档。

参数类型描述
minSizelong文件滚动的最小大小。默认值为 1,这将防止空文件滚动。
  • SizeBasedTriggeringPolicy(常用)

SizeBasedTriggeringPolicy 一旦文件达到指定大小,就会导致日志文件滚动 。可以以字节为单位指定大小,后缀为 KB、MB、GB 或 TB,例如20MB 大小还可以包含小数值,例如1.5 MB。当与基于时间的触发策略 TimeBasedTriggeringPolicy 结合使用时,filePattern 必须包含 %i 否则目标文件将在每次滚动时被覆盖,因为基于大小的触发策略 SizeBasedTriggeringPolicy 不会导致文件名中的时间戳值更改。

参数类型描述
sizeString文件滚动的大小。可以以字节为单位指定大小,后缀为 KB、MB、GB 或 TB,例如20 MB。大小还可以包含小数值,例如1.5 MB
  • TimeBasedTriggeringPolicy(常用)

当前时间与当前日志文件时间不匹配时,TimeBasedTriggeringPolicy 会触发归档。参数如下:

参数类型描述
intervalinteger基于filePattern中配置的最小时间单位进行来控制归档频率,默认值为1。如:filePattern中最小时间单位为小时,如果interval=1,则1小时归档一次;如果interval=2,则2小时归档一次。
modulateboolean默认为false。指明是否对interval进行调节,若modulate为true,会以0为开始对interval进行偏移计算。例如,最小时间粒度为小时,当前为3:00,interval为4,则后面归档时间依次为4:00,8:00,12:00,16:00
maxRandomDelayinteger指示随机延迟过渡的最大秒数。默认情况下,该值为0,表示没有延迟。此设置在配置了多个应用程序以同时滚动日志文件的服务器上很有用,并且可以在整个时间上分散这样做的负担。

DefaultRolloverStrategy(默认)

DefaultRolloverStrategy 通过接收filePattern属性中日期/时间模式(%d)和整数(%i)来控制归档方式。如果存在日期/时间模式,则将在归档时使用当前时间替换filePattern中配置的日期/时间部分,如果模式包含整数,则它将在每次归档时递增。如果归档时在模式中同时包含日期/时间和整数,则整数将递增,直到日期/时间部分也将被替换。如果文件模式以“ .gz”,“.zip”,“.bz2”,“.deflate”,“.pack200”或“ .xz”结尾,则将使用与后缀匹配的压缩方案来压缩文件。 bzip2, Deflate, Pack200 and XZ格式要求有Apache Commons Compress组件,另外xz格式还要求有XZ for Java组件。

DefaultRolloverStrategy参数如下:

参数类型描述
fileIndexString默认值为max。可选值为:min、max,2.8之后新增nomax。max: 最新滚动的日志放到文件名数值最大的文件中min: 最新滚动的日志放到文件名数值最小的文件中
mininteger日志文件数量的最小值。预设值为1。
maxinteger日志文件数量的最大值。一旦达到此值,较旧的归档文件将在以后的转换中被删除。预设值为7。
compressionLevelinteger将压缩级别设置为0-9,其中0 =无,1 =最佳速度,直到9 =最佳压缩。仅针对ZIP文件实现
tempCompressedFilePatternString压缩期间归档日志文件的文件名的模式。

示例说明:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" name="MyApp" packages="">
  <Appenders>
    <RollingFile name="RollingFile" 
									<!--  目录和文件不存在会自动创建      -->
                 fileName="logs/app.log" 
									<!--   按照年月日归档日志文件,并使用gzip压缩   -->
                 filePattern="logs/$${date:yyyy-MM}/app-%d{yyyy-MM-dd}-%i.log.gz">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
      </PatternLayout>
      <Policies>
				<!--   基于时间的滚动策略 filePattern 的最小时间单位是天,所以一天归档一次  -->
        <TimeBasedTriggeringPolicy />
				<!--   基于日志文件的大小进行归档,大小达到250MB,则归档      -->
        <SizeBasedTriggeringPolicy size="250 MB"/>
      </Policies>
			<!--   根据filePattern决定日志文件的名称,格式为:app-2022-09-20-1.log.gz,
      将在同一天 (1-20) 创建多达 20 个归档 -->
      <DefaultRolloverStrategy max="20"/>
    </RollingFile>
  </Appenders>
  <Loggers>
    <Root level="error">
      <AppenderRef ref="RollingFile"/>
    </Root>
  </Loggers>
</Configuration>
Layout

Appender 使用 Layout 将 LogEvent 转换为满足任何消费日志事件需求的格式。在 Log4j 1.x 和 Logback 中,将日志事件转换为字符串,而在 Log4j 2 中返回一个字节数组,这允许 Layout 的结果在更多类型的 Appender 中使用,比如可以输出日志到 socket 中,但是这意味着需要使用Charset配置大多数 Layout,以确保字节数组包含正确的值。Layout的根类 org.apache.logging.log4j.core.layout.AbstractStringLayout 默认使用的 Charset 为 UTF-8,每个继承 AbstractStringLayout 的 layout 可以提供自己的默认值。

Layout 的输出格式有如下几种,最灵活、最强大的是使用 Pattern Layout, 下面重点介绍 Pattern Layout:

Pattern Layout 的转换模式与 C 中 printf 函数的转换模式密切相关。转换模式由文字文本和称为转换说明符的格式控制表达式组成。请注意,任何文字文本(包括特殊字符)都可能包含在转换模式中,特殊字符包括\t、\n、\r、\f,使用\在输出中插入一个反斜杠。每个转换说明符都以百分号 (%) 开头,后跟可选的格式修饰符和转换字符,转换字符指定数据的类型,例如类别、优先级、日期、线程名称,格式修饰符控制字段宽度、填充、左右对齐等内容。举一个简单的例子说明一下,配置的 Pattern 如下:

<PatternLayout>
  <Pattern>%-5p [%t]: %m%n</Pattern>
</PatternLayout>

输出日志的语句如下:

Logger logger = LogManager.getLogger("MyLogger");
logger.debug("Message 1");
logger.warn("Message 2");

输出的日志格式如下:

DEBUG [main]: Message 1
WARN  [main]: Message 2

简单说明一下 %-5p [%t]: %m%n

  • %-5p:打印日志的级别,并且字符宽度为5,左对齐
  • %t:打印线程名称
  • %m:打印自定义的日志信息
  • %n:换行

一般使用基本配置就可以,pattern的配置释义如下:

参数描述
%c或%logger输出logName,如 Logger log = LoggerFactory.getLogger(“com.test.logName”); 则输出为“com.test.logName” ,如果格式为%c{参数},则输出内容参考官网:
%C或%class输出为所在类的全路径名
d{pattern}或date{pattern}输出时间,其中pattern可以是保留字,也可以是SimpleDateFormat中的字符。如 %d{DEFAULT} --> 2012-11-02 14:34:02,781 %d{DEFAULT_MICROS} --> 2012-11-02 14:34:02,123456 %d{yyyy-MM-dd HH:mm:ss.SSS} --> 2020-03-31 23:25:13.321 详见log4j PatternLayout
%F或%file输出所在类名.java,如所在类为com.test.LogTest,则输出为LogTest.java
%l输出错误的完整位置,全路径类名.方法名(类名.java:行号),如,com.test.LogTest.testLog(LogTest.java:31)
%L输出行号
%m或%msg或%message输出log.error(text)中的text内容
%M或%method输出方法名
%n换行符
%t或%thread输出线程名
%u{“RANDOM”“TIME”}或uuid输出uuid
%sn或%sequenceNumber输出自增序列
%r或%relative输出从JVM启动到当前时刻的毫秒数
%T或%tid或%threadId输出线程id
%t或%tn或%thread或%threadName输出线程id
%X{key[, key2...]} 或%mdc或%MDC输出生成日志所关联线程的 Thread Context Map(也叫做 MDC mapped diagnostic context )。%X{key,key2}中可以放一个或多个key,输出的值为 MDC 中 key 对应的value,如果有多个key,输出的格式为{key=val1,key2=val2},按照key的顺序输出。如果%X没有指定key,则会输出MDC中的所有键值对
%tp或%threadPriority输出线程优先级
%ex或%exception或%throwable输出绑定到日志事件的 Throwable 堆栈信息,默认情况下,这将输出完整的堆栈,就像通常通过调用 Throwable.printStackTrace() 找到的一样。
Logger

Logger 的名字大小写敏感,其命名有继承机制,比如:name 为 com.test.demo 的 Logger 会继承 name 为 com.test 的 Logger,所有的 Logger 都会继承 Root Logger。

Logger 的使用示例:

  <Logger name="org.example" level="info" additivity="false" includeLocation="false">
      <AppenderRef ref="STDOUT-APPENDER" />
  </Logger>

Logger 的常用参数:

参数类型描述
nameString指定Logger 的名字,有继承关系,如果它的名字是以点连接的,并且是另一个Logger名字的前缀,则称它为另一个Logger的祖先。比如 com.foo 是 com.foo.Bar 的祖先。
levelString指定Logger 的日志级别,可以填写:all、trace、debug、info、warn、error、fatal、all。
additivityboolean是否在父级的Logger 中打印日志,如果设置为true,则也会在父级Logger打印日志。默认为true。使用时建议设置为false。
includeLocationboolean是否包含位置信息,默认为false,开启打印位置信息会影响性能,建议设置为 false。

异步日志说明

异步logger和异步appender选一个就行,推荐使用异步logger(AsyncRoot),异步appender使用的是ArrayBlockingQueue,默认队列大小1024。异步logger使用的Disruptor环形队列和单独的处理线程,避免了锁的竞争,从而实现更高的吞吐量,队列大小默认4096。(需要引入disruptor依赖)

高性能必选(如果不是为了高性能,为什么不选logback呢~)

log4j2官方测试,asyncLogger相比asyncAppender有更好的表现,详细性能数据参考官方说明

Logger 有异步Logger 和 同步 Logger,使用异步Logger 时会单独开启线程记录日志,不会阻塞主线程,使用同步Logger记录日志时会阻塞主线程。

异步Logger是Log4j2新添加的功能,使用异步Logger能够提高记录日志的性能,异步Logger 内部使用 Disruptor (一个无锁的线程间通信库)而不是队列(ArrayBlockingQueue)实现的,它能够实现高吞吐低延时。

如果要使用异步Logger,需要在classpath中添加 disruptor.jar,如果使用的Log4j-2.9及以后的版本,需要添加 disruptor 的版本大于 disruptor-3.3.4.jar 使用maven添加如下依赖:

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>

可以选择使用全局异步Logger混合异步Logger

开启全局异步Logger

在classpath 中创建一个文件log4j2.component.properties,文件的内容如下:

log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

如果开启了全局异步Logger,在 log4j2.xml 文件中配置 <Root> 和 <Logger> 即可,AsyncLoggerContextSelector 会保证所有的 Logger 是异步的。

开启混合异步Logger

同步和异步Logger可以在 log4j2.xml 文件中一起配置使用,混合异步Logger 以稍差的性能损失(与全局异步相比)提供更加灵活的功能,在配置文件中使用 <AsyncRoot><AsyncLogger> 来开启异步Logger。

混合异步Logger使用示例如下:

<?xml version="1.0" encoding="UTF-8"?>
 
<!-- No need to set system property "log4j2.contextSelector" to any value
     when using <asyncLogger> or <asyncRoot>. -->
 
<Configuration status="WARN">
  <Appenders>
    <!-- Async Loggers will auto-flush in batches, so switch off immediateFlush. -->
    <RandomAccessFile name="RandomAccessFile" fileName="asyncWithLocation.log"
              immediateFlush="false" append="false">
      <PatternLayout>
        <Pattern>%d %p %class{1.} [%t] %location %m %ex%n</Pattern>
      </PatternLayout>
    </RandomAccessFile>
  </Appenders>
  <Loggers>
    <!-- pattern layout actually uses location, so we need to include it -->
    <AsyncLogger name="com.foo.Bar" level="trace" includeLocation="true">
      <AppenderRef ref="RandomAccessFile"/>
    </AsyncLogger>
    <Root level="info" includeLocation="true">
      <AppenderRef ref="RandomAccessFile"/>
    </Root>
  </Loggers>
</Configuration>

Java日志规范

1.应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架(SLF4J)中的API。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);

2. 日志工具对象logger应声明为private static final

  • 声明为private是出于安全性考虑,防止logger对象被其他类非法使用。
  • 将记录器成员声明为static变量需要更少的 CPU 时间,并且内存占用略小。
  • 声明为final是因为在类的生命周期内无需变更logger,只是记录该类的信息。

3. “+”来连接字符串,既不利于阅读,同时消耗了内存(heap memory),应该使用占位符:

反例:

if (logger.isDebugEnabled()) {
    logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
}

正例:(占位符)

logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol); 

4. 日志内容与日志级别相符合

  • DEBUG或者TRACE级别,比如方法调用参数,网络连接具体信息,一般是开发者调试程序使用,线上非特殊情况关闭这些日志
  • INFO级别,一般是比较重要却没有风险的信息,如初始化环境、参数,清理环境,定时任务执行,远程调用第一次连接成功
  • WARN级别,有可能有风险又不影响系统继续执行的错误,比如系统参数配置不正确,用户请求的参数不正确(要输出具体参数方便排查),或者某些耗性能的场景,比如一次请求执行太久、一条sql执行超过两秒,某些第三方调用失败,不太可能被运行的if分支等
  • ERROR级别,用于程序出错打印堆栈信息,不应该用于输出程序问题之外的其他信息。

5. 记录异常日志:

log.error("Error reading configuration file : "+ e.getMessage(), e);  // 推荐使用

必须包含e,不然会损失重要的StackTrace信息,难以定位问题发生地方。

6.不允许记录日志之后又抛出异常,因为有全局异常处理,会二次记录日志:

catch (Exception e) {
    logger.error("上传文件异常:\n" + e.getMessage());
    throw new ScException(ScErrorCode.CANNOT_SAVE_FILE_EXCEPTION, e);
}

7. 不要出现System print(包括System.out.println和System.error.println)语句。

推荐使用日志的info调试,例如logger.info("hello world!”)。

**8.避免重复打印日志,浪费磁盘空间,务必在log4j.xml中设置additivity=false。
**正例:

<logger name="com.demo" additivity="false">

Java 日志最佳实践

spring boot默认日志框架

因为 sofa boot 底层就是封装的 spring boot 所以这里查看 spring boot 的官方文档。

这里查看的是 spring boot 2.3.12.RELEASE 的官方文档进行说明

spring boot 日志相关特性总结

通过查看spring boot 官方文档中关于日志特性的说明总结如下内容:

spring boot 使用的是 JCL 记录内部的所有日志,但是底层的日志实现是开放的,并且为 JUL、Log4j2 和 Logback 提供了默认配置。在每种情况下,日志记录器都预先配置为使用控制台输出,并且还提供可选的文件输出。

默认情况下,spring boot 提供了一个starter spring-boot-starter-logging ,其依赖的情况如下图:

这里是使用 maven helper 插件进行查看的

可以看到 spring-boot-starter-logging 的日志实现是 logback ,并且提供了 jullog4j 的桥接器,以确保使用 Java Util Logging、Log4J 和 SLF4J 的依赖库都能正常工作。

默认情况下,Spring Boot 仅记录到控制台,不写入日志文件。如果您想在控制台输出之外写入日志文件,需要在 application.properties 文件中设置logging.file.name或者 logging.file.path属性。

自定义日志配置

通常情况下,我们不会使用spring boot 提供的默认日志配置,因为默认是输出到控制台的,虽然也可以通过spring 提供的配置在 application.properties 中指定输出到文件,但是sping 提供的那些配置的能力还是比较简单,无法进行定制化的配置。如果想实现更加细粒度的日志配置,那就需要使用日志实现的原生配置,例如 Logback 的 classpath:logback.xml,Log4j2 的 classpath:log4j2.xml 等。如果这些日志配置文件存在于 classpath 下,那么默认情况下,Spring Boot 就会自动加载这些配置文件。

因为日志系统是在 ApplicationContext创建之前被初始化的,所以无法从 @Configuration文件中的@PropertySources 控制日志记录,更改日志系统或完全禁用它的唯一方法是通过系统属性(环境变量)。

如何指定日志实现呢?

通过将日志框架的实现 jar 包添加到 classpath 中即可激活对应的日志系统,如果添加了多个日志实现,那么在应用启动的时候 SLF4J 会报警,并且随机选择一个日志实现,所以不要引人多个日志实现的,可以通过maven 的 <exclude>排除其他的日志实现。在 spring boot 中选择 Logback 或者 Log4j2 都可以,推荐 SLF4J+ Log4j2 的组合。

如何指定自定义的日志配置文件?

1、将日志配置文件放在根路径的 classpath 下面。

2、通过 spring 的属性配置 logging.config=classpath:log4j2-spring.xml

根据指定的日志实现,下面的日志配置文件将会被加载:

Logging SystemCustomization
Logbacklogback-spring.xml, logback-spring.groovy, logback.xml, logback.groovy
Log4j2log4j2-spring.xml , log4j2.xml
JDK (Java Util Logging)logging.properties

推荐使用 -spring 的配置文件,例如:log4j2-spring.xml,因为-spring 的配置文件提供了spring 特有的标签,这样spring 才能完全控制日志的初始化。

spring boot 使用 Log4j2

在创建Spring Boot工程时,我们引入了spring-boot-starter,其中包含了spring-boot-starter-logging,该依赖内容就是Spring Boot默认的日志框架Logback,所以我们在引入log4j2之前,需要先排除该包的依赖,再引入log4j2的依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

如项目中有导入spring-boot-starter-web依赖包记得去掉spring自带的日志依赖spring-boot-starter-logging

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

如果要开启异步日志,还需要导入 disruptor 的依赖:

  <dependency>
      <groupId>com.lmax</groupId>
      <artifactId>disruptor</artifactId>
      <version>3.4.4</version>
  </dependency>

log4j2-spring.xml 中使用 spring boot 配置

Spring Lookup 允许从 Log4j 配置文件 log4j2-spring.xml 中引用 Spring 配置文件中定义的属性,要使用该功能需要满足如下几个条件:

1、需要是 spring boot 应用

2、需要在classpath 中添加 log4j2-spring.xml 文件

3、需要添加 log4j-spring-boot 的依赖

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-spring-boot</artifactId>
</dependency>

log4j2-spring.xml 中引用 Spring 配置文件中定义的属性的方式为: ${spring:spring.application.name}

<!-- 会将 spring.application.name 的值放到 applicationName 这个变量中
后续可以通过 ${applicationName} 使用该变量的值
-->
<property name="applicationName">${spring:spring.application.name}</property>


<File name="Application" fileName="application-${spring:profiles.active[0]}.log">
  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] $${spring:spring.application.name} %m%n</pattern>
  </PatternLayout>
</File>

log4j2-spring.xml 中使用 sofa boot 配置

sofaboot 也允许 log4j2-spring.xml 中引用 Spring 配置文件中定义的属性,但是 sofaboot 是通过 ${ctx: spring.application.name } 进行引用到的。查看log4j官网可以知道, ${ctx:name} 是从 MDC 中读取内容的,

sofaboot 是怎么把spring 的配置属性放到 Thread Context Map 中的呢,抱着好奇的心态查看了源码,整个分析过程如下,sofaboot 中导入了 log4j2-alipay-sofa-boot-starter ,其中有一个类 com.alipay.sofa.log4j2.spring.AlipayLog4j2SpringContextListener继承了 GenericApplicationListener,可以看到如下一段代码,就是用来把 spring 的配置文件放到 ThreadContext 中,在 log4j2-spring.xml 就可以通过 ${ctx:name} 读取 spring 配置文件的内容:

使用MDC存储上面说的k-v键值对,配合监听spring的事件,微服务工程启动后,加载好application.properties 时,会自动广播事件。

log4j2-spring.xml 文件配置

配置说明

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Properties>
        <!--    指定日志记录存放的目录        -->
        <Property name="logDir">${ctx:logging.path}/${ctx:spring.application.name}</Property>
        <!--    指定日志的输出格式       -->
        <Property name="logPattern">%d [%X{traceId} %X{rpcId} - %X{loginUserEmail}/%X{loginUserID}/%X{remoteAddr}/%X{clientId} - %X{requestURIWithQueryString}] %-5p %T [%10t] %-40c{2} : %m%n%throwable</Property>
        
        <!--    指定应用日志级别    -->
        <Property name="logLevel">${ctx:logging.level.com.alipay.demo}</Property>
        <!--    指定日志文件保存天数    -->
        <Property name="maxLogFileCount">15</Property>
        
        
        <!--    指定记录数据访问层日志的包路径和文件名        -->
        <Property name="dalLog">com.alipay.demo.client</Property>
        <Property name="dalLogFileName">dal</Property>
        <!--    指定记录业务日志的包路径和文件名       -->
        <Property name="bizLog">com.alipay.demo.service</Property>
        <Property name="bizLogFileName">biz</Property>
        <!--    指定记录访问facade日志的包路径和文件名        -->
        <Property name="facadeLog">com.alipay.demo.facade</Property>
        <Property name="facadeLogFileName">facade</Property>
    </Properties>
    <Appenders>
        <Console name="STDOUT-APPENDER" target="SYSTEM_OUT">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
        </Console>
        
        <!--   错误日志,记录非预期的错误日志,记录该文件错误日志会触发告警     -->
        <RollingFile name="ERROR-APPENDER" fileName="${logDir}/common-error.log"
                     filePattern="${logDir}/common-error.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <TimeBasedTriggeringPolicy/>
            <!--    最多保存max个文件  -->
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
        </RollingFile>
        <!--      -->
        <RollingFile name="APP-DEFAULT-APPENDER" fileName="${logDir}/app-default.log"
                     filePattern="${logDir}/app-default.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy/>
        </RollingFile>

        <RollingFile name="SPRING-APPENDER" fileName="${ctx:logging.path}/spring/spring.log"
                     filePattern="${ctx:logging.path}/spring/spring.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy/>
        </RollingFile>

        <RollingFile name="NO-USAGE-APPENDER" fileName="${ctx:logging.path}/no-usage/no-usage.log"
                     filePattern="${ctx:logging.path}/no-usage/no-usage.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy/>
        </RollingFile>

        <!-- ===========================数据访问层日志============================= -->
        <RollingFile name="dal-info-appender" fileName="${logDir}/${dalLogFileName}-info.log"
                     filePattern="${logDir}/${dalLogFileName}-info.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--     只记录info级别的日志,warn、error级别日志都不记录               -->
                <ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <RollingFile name="dal-warn-appender" fileName="${logDir}/${dalLogFileName}-warn.log"
                     filePattern="${logDir}/${dalLogFileName}-warn.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--     只记录warn级别的日志,error级别日志都不记录               -->
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <RollingFile name="dal-error-appender" fileName="${logDir}/${dalLogFileName}-error.log"
                     filePattern="${logDir}/${dalLogFileName}-error.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--   只记录error级别日志             -->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <!-- ===========================业务日志============================= -->
        <RollingFile name="biz-info-appender" fileName="${logDir}/${bizLogFileName}-info.log"
                     filePattern="${logDir}/${bizLogFileName}-info.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--     只记录info级别的日志,warn、error级别日志都不记录               -->
                <ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <RollingFile name="biz-warn-appender" fileName="${logDir}/${bizLogFileName}-warn.log"
                     filePattern="${logDir}/${bizLogFileName}-warn.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--     只记录warn级别的日志,error级别日志都不记录               -->
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <RollingFile name="biz-error-appender" fileName="${logDir}/${bizLogFileName}-error.log"
                     filePattern="${logDir}/${bizLogFileName}-error.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--   只记录error级别日志             -->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <!-- ===========================facade日志============================= -->
        <RollingFile name="facade-info-appender" fileName="${logDir}/${facadeLogFileName}-info.log"
                     filePattern="${logDir}/${facadeLogFileName}-info.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--     只记录info级别的日志,warn、error级别日志都不记录               -->
                <ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <RollingFile name="facade-warn-appender" fileName="${logDir}/${facadeLogFileName}-warn.log"
                     filePattern="${logDir}/${facadeLogFileName}-warn.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--     只记录warn级别的日志,error级别日志都不记录               -->
                <ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>

        <RollingFile name="facade-error-appender" fileName="${logDir}/${facadeLogFileName}-error.log"
                     filePattern="${logDir}/${facadeLogFileName}-error.log.%d{yyyy-MM-dd}"
                     append="true">
            <PatternLayout charset="UTF-8">
                <Pattern>${logPattern}</Pattern>
            </PatternLayout>
            <!--   根据filePattern的配置 dd:表示一天保存一个文件  -->
            <TimeBasedTriggeringPolicy/>
            <DefaultRolloverStrategy max="${maxLogFileCount}"/>
            <!--    过滤记录info级别的日志    过滤条件有三个值:ACCEPT(接受),DENY(拒绝),NEUTRAL(后续过滤器处理)        -->
            <Filters>
                <!--   只记录error级别日志             -->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>


    </Appenders>

    <Loggers>
        <AsyncLogger name="STDOUT" additivity="false" level="info">
            <AppenderRef ref="STDOUT-APPENDER"/>
        </AsyncLogger>
        

        <AsyncLogger name="com.taobao.tair" additivity="false" level="${logLevel}">
            <AppenderRef ref="NO-USAGE-APPENDER"/>
        </AsyncLogger>

        <AsyncLogger name="com.taobao.vipserver" additivity="false" level="${logLevel}">
            <AppenderRef ref="NO-USAGE-APPENDER"/>
        </AsyncLogger>

        <AsyncLogger name="com.taobao.remoting" additivity="false" level="${logLevel}">
            <AppenderRef ref="NO-USAGE-APPENDER"/>
        </AsyncLogger>

        <AsyncLogger name="com.alipay.demo.acts.test" additivity="false" level="${logLevel}">
            <AppenderRef ref="APP-DEFAULT-APPENDER"/>
            <AppenderRef ref="ERROR-APPENDER"/>
            <AppenderRef ref="STDOUT-APPENDER"/>
        </AsyncLogger>

        <AsyncLogger name="org.springframework" additivity="false" level="${logLevel}">
            <AppenderRef ref="ERROR-APPENDER"/>
            <AppenderRef ref="SPRING-APPENDER"/>
        </AsyncLogger>
        <!--   没有在前面定义的包路径下的日志记录到这里     -->
        <AsyncLogger name="com.alipay.demo" additivity="false" level="${logLevel}">
            <AppenderRef ref="APP-DEFAULT-APPENDER"/>
            <AppenderRef ref="ERROR-APPENDER"/>
            <AppenderRef ref="STDOUT-APPENDER"/>
        </AsyncLogger>

        <!--    数据访问层日志记录器        -->
        <AsyncLogger name="${dalLog}" additivity="false" level="${logLevel}">
            <!--      不要在多个日志文件中记录日志,因为查看日志的时候不方便          -->
            <AppenderRef ref="dal-info-appender"/>
            <AppenderRef ref="dal-warn-appender"/>
            <AppenderRef ref="dal-error-appender"/>
            <AppenderRef ref="STDOUT-APPENDER"/>
        </AsyncLogger>


        <!--    业务日志记录器        -->
        <AsyncLogger name="${bizLog}" additivity="false" level="${logLevel}">
            <!--      不要在多个日志文件中记录日志,因为查看日志的时候不方便          -->
            <AppenderRef ref="biz-info-appender"/>
            <AppenderRef ref="biz-warn-appender"/>
            <AppenderRef ref="biz-error-appender"/>
            <AppenderRef ref="STDOUT-APPENDER"/>
        </AsyncLogger>


        <!--    facade日志记录器        -->
        <AsyncLogger name="${facadeLog}" additivity="false" level="${logLevel}">
            <!--      不要在多个日志文件中记录日志,因为查看日志的时候不方便          -->
            <AppenderRef ref="facade-info-appender"/>
            <AppenderRef ref="facade-warn-appender"/>
            <AppenderRef ref="facade-error-appender"/>
            <AppenderRef ref="STDOUT-APPENDER"/>
        </AsyncLogger>


        <AsyncRoot level="${logLevel}">
            <AppenderRef ref="APP-DEFAULT-APPENDER"/>
            <AppenderRef ref="ERROR-APPENDER"/>
            <AppenderRef ref="STDOUT-APPENDER"/>
        </AsyncRoot>
    </Loggers>
</Configuration>

详细说明上面配置的设计思路,总体思路是:按照包路径分层记录日志,并且日志文件按照不同级别的日志记录分类。

指定需要记录日志的包路径,将要记录的日志分成三层:

  • 数据访问层,记录数据访问的日志,比如第三方 rpc 调用日志,访问数据库日志等
  • 业务层,记录业务逻辑处理相关的日志
  • facade层,记录外部调用facade接口的日志

日志文件按照日志级别分为三类:

  • info,只记录info级别日志
  • warn,只记录warn级别日志
  • error,只记录error级别日志

日志文件生成如下所示:

如果记录日志的类没有在日志配置中指定的包路径下,则默认使用 <AsyncRoot> 的配置记录到

app-default.log 和 common-error.log 文件中。

如果有其他目录的日志需要单独记录,按照上面的配置继续添加日志文件即可。

如何记录日志

记录日志非常简单,只需要使用 slf4j 接口记录日志即可,使用如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger LOGGER = LoggerFactory.getLogger(Demo.class);

因为 Log4j 中 Logger 的名字有继承关系,可以根据名字的前缀来区分父子关系,所以 LOGGER 的构造方法参数是Class,这样能够根据类的结构来进行区分日志。这也是前面配置包路径的原因,在该包路径中的类能够找到前缀最匹配的父 Logger 记录日志。

参考文档

Java日志进化论

SLF4J 官方文档

Logback 官方文档

Log4j2官方文档

spring boot 日志规范

Log4j2 配置文件详解

spring boot 中使用 log4j2