你知道 log4j2 各项配置的全部含义吗?带你了解 log4j2 的全部组件

2022-06-27 16:22:55 浏览数 (1)

1. 引言

此前的文章中通过 log4j2 AsyncAppender 的源码介绍了异步日志的用法:

log4j2 异步日志 -- AsyncAppender

有读者私信我表示想让我写一篇关于 log4j2 工作原理和用法的文章,那么,本文我们就来详细了解一下。

对于服务端程序来说,其运行状态时刻的监控是十分必要的,而所有监控手段中,最基本和最重要的手段 -- 日志的重要性毋庸多言。在日志的帮助下,我们可以轻松地获得有关应用程序中发生的情况的信息,保存现场、复现问题、解决问题。

在 java 中,存在着很多日志框架,诸如 log4j、logback,以及在他们基础上的改进版 log4j2,此前的文章中也已经介绍过,log4j2 凭借其技术改进,引入无锁异步等机制让日志吞吐量、性能都有大幅提升,从而能够脱颖而出。

那么,我们要如何配置和使用 log4j2 呢?

2. log4j2 最基本的使用

log4j2 已经做到了开箱即用。

如果你使用 maven 管理项目,只需要在 pom.xml 中配置:

代码语言:javascript复制
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.2</version>
</dependency>

如果你使用的是 Gradle,那么按照以下配置:

代码语言:javascript复制
dependencies {
  compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.8.2'
}

接着,你只需要在代码中创建 logger:

代码语言:javascript复制
private static Logger logger = LogManager.getLogger(MyService.class);

然后使用 logger:

代码语言:javascript复制
logger.error("This is an error message");

3. 自定义配置

log4j2 之所以能够做到开箱即用,实际上是他提供了默认的一套配置,而大部分情况下,我们需要自己创建自定义的配置,来满足我们不同的实际需要。

log4j2 支持 xml、json、yaml 以及 .properties 等多种配置方式,我们最常用的一般是使用 xml 格式的配置,只需要将 log4j2.xml 放到代码的 classpath 下,log4j2 组件就会自动读取和应用相应的配置。

比如下面就是一个典型的 xml 配置:

代码语言:javascript复制
<?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="INFO">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

也许你乍看之下会觉得这个配置有些复杂,没关系,下面我们就来深入介绍一下。

除了第一行的 xml 基本信息的声明外,其余的部分就是 log4j2 配置的所有内容了。

最外层的 Configuration 标签指定了日志应该被记录的默认级别。

Configuration 内部声明了我们需要使用的 Appender、Layout 以及由此配置而来的 logger。

4. log4j2 的组件

如图所示,log4j2 由四部分构成:

  • Logger -- 负责捕获日志记录,并传递给 Appender,他是日志行为的发起者。
  • Appender -- 负责将日志事件进行分类处理,将日志发往他应该去的目标去向,因此也可以称为 Handler。
  • Layout -- Layout 负责在日志输出前决定日志的格式,因此也可以称为 Fomatter。
  • Filter -- Filter 是可选的组件,每一个 Logger、Appender 甚至全局都可以配置若干个 Filter,来决定相应的组件对当前的日志时间是否关心。

这样一来,我们再来看上面的配置,就非常清楚了。

上述配置中,配置了一个 Logger,用来打印 INFO 级别的日志,而他使用的 Appender 是名为 Console 的 Appender。

这个名为 Console 的 Appender 是一个 ConsoleAppender,他的功能是向某个指定的地方输出日志内容,根据配置,这个指定的地方是标准输出。

配置中指定,ConsoleAppender 使用 PatternLayout 来对日志内容进行格式化。

5. 常用的 Appender

Appender 是一个路由 LogEvent 的管道,他决定了日志要去哪以及怎么去。

5.1 最基本的 Appender -- ConsoleAppender 与 FileAppender

ConsoleAppender 与 FileAppender 顾名思义,就是向控制台或文件输出日志。

最基本的配置只需要添加 target 参数指明输出位置,ConsoleAppender 的 target 可选 SYSTEM_OUT 或 SYSTEM_ERR,FileAppender 的 target 就是文件地址。

  • 示例
代码语言:javascript复制
<Console name="STDOUT" target="SYSTEM_OUT">
  <PatternLayout pattern="%m%n"/>
</Console>

5.2 最常用的 Appender -- RollingFileAppender

对于一个线上持续工作的服务来说,持续向单个文件输出日志显然是不现实的。

RollingFileAppender 实现了滚动式的文件存储,他有三个策略:

  1. OnStartupTriggeringPolicy -- 每次 JVM 启动,都滚动到新的日志文件开始记录。
  2. TimeBasedTriggeringPolicy -- 根据日期时间进行滚动。
  3. SizeBasedTriggeringPolicy -- 按照日志文件大小进行滚动。
  • 示例
代码语言:javascript复制
<RollingFile name="RollingFileAppender" fileName="logs/app.log"
      filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz">
  <PatternLayout>
    <Pattern>%d [%t] %p %c - %m%n</Pattern>
  </PatternLayout>
  <Policies>
    <OnStartupTriggeringPolicy />
    <TimeBasedTriggeringPolicy />
    <SizeBasedTriggeringPolicy size="50 MB" />
  </Policies>
  <DefaultRolloverStrategy max="20" />
</RollingFile>

5.3 写入数据库的 appender -- JDBCAppender

除了写入文件外,可能你需要将日志写入数据库,log4j2 也同样提供了相应的 Appender:

代码语言:javascript复制
<JDBC name="JDBCAppender" tableName="logs">
  <DataSource jndiName="java:/comp/env/jdbc/LoggingDataSource" />
  <Column name="date" isEventTimestamp="true" />
  <Column name="logger" pattern="%logger" />
  <Column name="level" pattern="%level" />
  <Column name="message" pattern="%message" />
  <Column name="exception" pattern="%ex{full}" />
</JDBC>

5.4 失败后的处理 -- FailoverAppender

无论是写入文件还是写入数据库,都是有可能写入失败的,对一个线上服务来说,一旦出现日志写入失败,就会造成现场丢失的严重问题。

log4j2 提供了失败处理的 appender -- FailoverAppender。

  • 示例
代码语言:javascript复制
<Failover name="FailoverAppender" primary="JDBCAppender">
    <Failovers>
        <AppenderRef ref="RollingFileAppender" />
        <AppenderRef ref="Console" />
    </Failovers>
</Failover>

对于生产环境来说,有失败备用方案总是一件好事情。

5.5 异步日志 -- AsyncAppender

日志性能总是我们非常关心的一个问题,log4j2 也正是因为有了异步日志机制才能够脱颖而出。

此前的文章已经进行过充分介绍,下面是一个配置的例子:

代码语言:javascript复制
<Async name="Async">
  <AppenderRef ref="RollingFileAppender" />
  <AppenderRef ref="Console" />
</Async>

5.6 其他 Appender

log4j2 还提供了其他一些实用的 Appender 供你选择:

  • FlumeAppender -- 将几个不同源的日志汇集、集中到一处
  • JMSQueueAppender & JMSTopicAppender -- JMS 相关的日志输出。
  • RewriteAppender -- 对日志事件进行掩码注入。
  • RoutingAppender -- 允许通过规则路由日志到不同的输出地。
  • SMTPAppender -- 通过邮件发送日志。
  • SocketAppender -- 以 socket 的方式发送到远程主机。

6. 常用的 Layout

Appender 解决了日志打印到哪里的问题,而 Layout 则解决日志如何打印,也就是日志格式问题,这也就是 Layout 也被称为 Formatter 的原因。

log4j2 也同样提供了多种多样的,用来实现打印各种格式日志的丰富 Layout:

  • CSVLayout
  • JSONTamplateLayout
  • JSONLayout
  • GelfLayout
  • HtmlLayout
  • SerializedLayout
  • XMLLayout
  • YAMLLayout
  • PatternLayout

他们的配置方式都很简单,例如我们最常用的 PatternLayout 可以这样配置:

代码语言:javascript复制
<Console name="STDOUT" target="SYSTEM_OUT">
  <PatternLayout pattern="%m%n"/>
</Console>

你也可以通过通过 PatternLayout 实现其他各种格式日志输出的效果,例如 json 日志:

代码语言:javascript复制
<Console name="STDOUT" target="SYSTEM_OUT">
  <PatternLayout>
    <pattern>{"level":"%level","thread":"%t","date":"%d{yyyyMMdd HH:mm:ss.SSS}","message":"%m","logger":"%logger"}%n</pattern>
  </PatternLayout>
</Console>

7. 常用的 Filter

Filter 是可选的,log4j2 会在日志产生时自动调用预先配置的 Filter 的 filter 方法进行过滤,以便获得是否允许打印的标识。

是否允许打印的标识是一个 Result 类型的枚举,他的值有三种:

  1. ACCEPT
  2. DENY
  3. NEUTRAL

这里特殊讲一下 NEUTRAL,如果只有一个 Filter,那么 NEUTRAL 与 ACCEPT 没有任何区别,只有在多个 Filter 级联使用时,NEUTRAL 才有意义,他表示由下一个 filter 决定是否 ACCEPT。

通常 filter 并不直接决定最终的结果,因为不同的场景下,filter 命中后的行为并不一定相同,因此,filter 只返回命中或未命中,然后由业务具体需要决定是否允许打印相应的日志是更好的选择。

log4j2 的 Filter 就是基于上述原则创建的,他提供了 onMatch 与 onMisMatch 两个参数供用户配置,filter 值返回当前场景命中(onMatch)或未命中(onMisMatch)

Log4j2 允许你将 Filter 配置为全局有效或对某个 Appender 生效。

7.1 控制日志打印速度 --BurstFilter

BurstFilter 可以控制每秒日志量,对于超过数量的日志进行丢弃。

它包含一个 rate 参数,表示每秒最大日志数。maxBurst 参数则表示在开始过滤前允许多少条日志请求。

  • 实例
代码语言:javascript复制
<RollingFile name="RollingFile" fileName="logs/app.log"
                 filePattern="logs/app-%d{MM-dd-yyyy}.log.gz">
  <BurstFilter level="INFO" rate="16" maxBurst="100"/>
  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] %m%n</pattern>
  </PatternLayout>
  <TimeBasedTriggeringPolicy />
</RollingFile>

7.2 级联 Filter -- CompositeFilter

上文已经提到,log4j2 是允许 filter 的级联的,CompositeFilter 就是这一功能的实现,我们只需要配置 Filters 标签即可:

代码语言:javascript复制
<Filters>
  <Marker marker="EVENT" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
  <DynamicThresholdFilter key="loginId" defaultThreshold="ERROR"
                          onMatch="ACCEPT" onMismatch="NEUTRAL">
    <KeyValuePair key="User1" value="DEBUG"/>
  </DynamicThresholdFilter>
</Filters>

7.3 动态日志级别设置 --DynamicThresholdFilter

很多时候,我们需要借助更多的日志来进行问题排查,但过多的日志又势必会对线上服务的性能以及磁盘等资源造成压力,此时有一个好的选择,那就是打印丰富的 debug 级别的日志,而 logger 的 level 至少定义在 info 级别以上,这样实际上在生产环境中,这些 debug 级别的日志并不会被打印出来,而在测试环境中,只需要改变 logger 的 level 就可以打开这些 debug 日志的打印,方便我们排查问题。

有时我们更想要知道线上场景下究竟发生了什么,但现实情况我们又不能让所有人都打印出 debug 级别的日志,有什么办法只让符合条件的请求打印出 debug 级别的日志吗?log4j2 用 DynamicThresholdFilter 解决了这一问题。

代码语言:javascript复制
<DynamicThresholdFilter key="loginId" defaultThreshold="ERROR"
                        onMatch="ACCEPT" onMismatch="NEUTRAL">
  <KeyValuePair key="User1" value="DEBUG"/>
</DynamicThresholdFilter>

这个配置表示,默认日志级别为 ERROR 级别,但符合 MDC.get("loginId") 为 User1 的请求日志级别为 DEBUG。

这样,我们只需要在日志打印前执行 MDC.put("loginId", "User1") 就可以实现动态改变本次请求的日志级别了,这对于线上 vip 用户问题的排查是十分方便的。

7.4 限制时间的 filter -- TimeFilter

TimeFilter 允许只在一天中的指定时间进行日志记录:

代码语言:javascript复制
<RollingFile name="RollingFile" fileName="logs/app.log"
                 filePattern="logs/app-%d{MM-dd-yyyy}.log.gz">
  <TimeFilter start="05:00:00" end="05:30:00" onMatch="ACCEPT" onMismatch="DENY"/>
  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] %m%n</pattern>
  </PatternLayout>
  <TimeBasedTriggeringPolicy />
</RollingFile>

7.5 过滤不同类型的日志 -- MarkerFilter

有的时候,我们希望根据日志中的标记来决定不同的日志输出到不同的位置。MarkerFilter 实现了这一功能。

代码语言:javascript复制
<RollingFile name="RollingFile" fileName="logs/app.log"
                 filePattern="logs/app-%d{MM-dd-yyyy}.log.gz">
  <MarkerFilter marker="FLOW" onMatch="ACCEPT" onMismatch="DENY"/>
  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] %m%n</pattern>
  </PatternLayout>
  <TimeBasedTriggeringPolicy />
</RollingFile>

这个配置只允许带有 FLOW 标记的日志通过并进行记录。

7.6 其他 FILTER

log4j2 还支持其他更加丰富的 Filter 类来支持各种强大的过滤功能:

  • MapFilter -- 与 DynamicThresholdFilter 类似,MapFilter 通过配置 kv 实现 MDC.get("key") == value 的情况下进行日志打印。
  • RegexFilter -- 支持正则表达式的 MarkerFilter
  • ScriptFilter -- 允许用户编写自己的 js 或 groovy 脚本决定是否 onMatch
  • ThreadContextMapFilter -- DynamicThresholdFilter 的线程上下文版本。

8. 后记

log4j2 提供了如此多种功能的 appender、layout、filter,通过他们之间各种各样的组合可以满足一个又一个特殊的使用场景。

但问题在于,无论 log4j2 提供了多么强大的功能,都无法保证能够完美覆盖所有的场景,那么,当我们遇到了上述所有支持的功能所无法满足的场景时,我们应该如何去解决呢?

幸运的是,log4j2 支持我们创建自己的 Appender、Layout、Filter 以便实现我们极具个性化的自定义功能。那么,如何创建自己的 Appender、Layout、Filter 呢?敬请期待博主的下一篇文章。

附录 -- 参考资料

https://logging.apache.org/log4j/log4j-2.5/index.html

0 人点赞