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 就是文件地址。
- 示例
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%m%n"/>
</Console>
5.2 最常用的 Appender -- RollingFileAppender
对于一个线上持续工作的服务来说,持续向单个文件输出日志显然是不现实的。
RollingFileAppender 实现了滚动式的文件存储,他有三个策略:
- OnStartupTriggeringPolicy -- 每次 JVM 启动,都滚动到新的日志文件开始记录。
- TimeBasedTriggeringPolicy -- 根据日期时间进行滚动。
- 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:
代码语言: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。
- 示例
<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 类型的枚举,他的值有三种:
- ACCEPT
- DENY
- 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 标签即可:
代码语言: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