log4j2 的构成组件

2021-03-13 11:48:58   最后更新: 2021-03-13 11:48:58   访问数量:116




 

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

 

log4j2 异步日志(一) -- AsyncAppender

 

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

 

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

 

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

 

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

 

 

 

 

log4j2 已经做到了开箱即用。

 

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

 

<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency>

 

 

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

 

dependencies { compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.8.2' }

 

 

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

 

private static Logger logger = LogManager.getLogger(MyService.class);

 

 

然后使用 logger:

 

logger.error("This is an error message");

 

 

 

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

 

log4j2 支持 xml、json、yaml 以及 .properties 等多种配置方式,我们最常用的一般是使用 xml 格式的配置,只需要将 log4j2.xml 放到代码的 classpath 下,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="INFO"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>

 

 

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

 

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

 

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

 

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

 

 

 

 

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

 

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

 

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

 

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

 

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

 

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

 

 

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

 

5.1 最基本的 Appender -- ConsoleAppender 与 FileAppender

 

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

 

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

 

  • 示例

 

<Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout pattern="%m%n"/> </Console>

 

 

5.2 最常用的 Appender -- RollingFileAppender

 

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

 

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

 

  1. OnStartupTriggeringPolicy -- 每次 JVM 启动,都滚动到新的日志文件开始记录。
  2. TimeBasedTriggeringPolicy -- 根据日期时间进行滚动。
  3. SizeBasedTriggeringPolicy -- 按照日志文件大小进行滚动。

 

  • 示例

 

<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:

 

<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。

 

  • 示例

 

<Failover name="FailoverAppender" primary="JDBCAppender"> <Failovers> <AppenderRef ref="RollingFileAppender" /> <AppenderRef ref="Console" /> </Failovers> </Failover>

 

 

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

 

5.5 异步日志 -- AsyncAppender

 

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

 

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

 

<Async name="Async"> <AppenderRef ref="RollingFileAppender" /> <AppenderRef ref="Console" /> </Async>

 

 

5.6 其他 Appender

 

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

 

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

 

 

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

 

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

 

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

 

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

 

<Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout pattern="%m%n"/> </Console>

 

 

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

 

<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>

 

 

 

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 参数则表示在开始过滤前允许多少条日志请求。

 

  • 实例

 

<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 标签即可:

 

<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 解决了这一问题。

 

<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 允许只在一天中的指定时间进行日志记录:

 

<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 实现了这一功能。

 

<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 的线程上下文版本。

 

 

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

 

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

 

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

 

 

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

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

java 专题






filter      logger      log4j2      appender      layout     


京ICP备2021035038号