loong - Log4j2 - 配置
该文章配置基于springBoot,并添加关于异步日志配置和关于控制台报错问题处理
日志介绍
- 同步日志
- 混合同步和异步日志
- 异步日志(性能最好,推荐使用)异步日志情况下,增加 Disruptor 队列长度并配置队列堵塞丢弃策略从可以增加高并发下的性能,实现如下:
- jvm 参数:-DLog4jAsyncQueueFullPolicy=Discard -DLog4j2.asyncLoggerRingBufferSize:指定队列的长度(根据实际压测情况调试,一般不会指定长度)
- 或者在log4j2.component.properties中配置丢弃策略:
- log4j2.asyncLoggerRingBufferSize=根据实际压测情况调试
同步日志
步骤
- 添加依赖
- 解决依赖冲突
- 添加配置文件
- 激活配置文件
添加依赖
<!-- log4j2日志框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- log4j2 异步日志需要引入这个jar包 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
解决依赖冲突
springboot 默认引入
spring-boot-starter-logging
与我们引入的spring-boot-starter-log4j2
的依赖冲突了,需要我手动排除(exclusion)spring-boot-starter-logging
依赖
# 这个报错说发现了多个slf4j绑定
SLF4J: Class path contains multiple SLF4J bindings.
# 一个绑定是 slf4j 和 logback-classic
SLF4J: Found binding in [jar:file:/xxx/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
# 还有一个# 一个绑定是 slf4j 和 log4j-slf4j-impl
SLF4J: Found binding in [jar:file:/xxxx/org/apache/logging/log4j/log4j-slf4j-impl/2.17.2/log4j-slf4j-impl-2.17.2.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
# 生效的binding是logback.classic
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
解决方案
<!--核心启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<!--如果要使用log4j2,就需要把这个排除,不然控制台会报错-->
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
创建配置文件
在resources文件下创建log4j2.xml文件
配置文件加载顺序
Log4j 包含 4 种 ConfigurationFactory 的实现,分别适用于 JSON、YAML、properties 和 XML 配置文件。在 Log4j 启动时可以按照以下顺序自动加载配置文件:
- 查找
log4j.configurationFile
系统属性所指定的配置文件名,如果该系统属性值存在,就尝试使用相应文件扩展名的 ConfigurationFactory 来加载指定的配置文件。通过在代码中调用System.setProperties("log4j.configurationFile","FILE_PATH")
或者将-Dlog4jconfigurationFile=file://C:/configuration.xml
参数传递给 JVM; - 如果没有找到,则 properties ConfigurationFactory 就在 classpath 中寻找 log4j2-test.properties 配置文件;
- 如果没有找到,则 YAML ConfigurationFactory 就在 classpath 中寻找 log4j2-test.yaml 或 log4j2-test.yml 配置文件;
- 如果没有找到,则 JSON ConfigurationFactory 就在 classpath 中寻找 log4j2-test.json 或 log4j2-test.jsn 配置文件;
- 如果没有找到,则 XML ConfigurationFactory 就在 classpath 中寻找log4j2-test.xml 配置文件;
- 如果没有找到测试配置文件,则 properties ConfigurationFactory 就在 classpath 中寻找 log4j2.properties 配置文件;
- 如果没有找到,则 YAML ConfigurationFactory 就在 classpath 中寻找 log4j2.yaml 或 log4j2.yml 配置文件;
- 如果没有找到,则 JSON ConfigurationFactory 就在 classpath 中寻找 log4j2.json 或 log4j2.jsn 配置文件;
- 如果没有找到,则 XML ConfigurationFactory 就在 classpath 中寻找log4j2.xml 配置文件;
- 如果上面的配置文件都没有找到,就使用默认的 DefaultConfiguration 配置。
log4j2.xml配置内容
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="WARN" monitorInterval="30">
<!--
- %d{yyyy-MM-dd HH:mm:ss.SSS} : 日志生成时间,输出格式为“年-月-日 时:分:秒.毫秒”
- %p : 日志输出格式
- %c : logger的名称
- %T %tid %threadId : 线程号 非常有必要
- %t %tn %thread %threadName : 输出日志的线程名称,类似于线程号作用相同
- %L %line : 显示日志输出的代码所在的行数,输出时会检查堆栈信息(耗时操作**)
- %M %method : 输出方法名 输出时会检查堆栈信息(耗时操作**)
- %m %msg : 输出应用中自定义的日志内容,即 logger.info("message")
- %xEx : 异常信息
- %n : 换行符
-->
<!--properties:设置全局变量 -->
<properties>
<!--LOG_HOME:指定当前日志存放的目录 -->
<property name="LOG_HOME">D:/loong-log</property>
<property name="PROJECT_NAME">LOONG-TEST</property>
<property name="LOG__NAME_TEMP">LOG-TEMP</property>
<property name="LOG_NAME_INFO">LOG-INFO</property>
<!--[年月日 时分秒.毫秒] [线程Id] [线程名称] [日志级别] [全限定类名] [日志具体内容] [异常信息]-->
<property name="LOG_PATTERN">[%d{yyyy-MM-dd HH:mm:ss.SSS}] [threadId=%tid] [%threadName] [%-5level] [%c] - %msg %xEx %n</property>
</properties>
<!-- appender:定义日志输出目的地,内容和格式等 -->
<!--
Appender: 定义日志输出的目的地,内容,格式
-ConsoleAppender 将日志消息输出到控制台(用到了)
-FileAppender 将日志消息输出到文件(用到了)
-RollingFileAppender 将日志消息输出到文件,并支持日志轮换(用到了)
-SocketAppender 将日志消息发送到远程服务器的 Socket 端口
-JMSAppender 将日志消息发送到 JMS 队列或主题
-SMTPAppender 将日志作为电子邮件发送
-SyslogAppender 将日志输出到 Syslog 服务器
-JDBCAppender 将日志写入数据库
-KafkaAppender 将日志消息发送到 Kafka 主题
-->
<appenders>
<!--target="SYSTEM_OUT" 通过system.out输出的东西 encoding:日志编码 immediateFlush:是否立即刷新到控制台(默认true)-->
<console name="consoleAppender" target="SYSTEM_OUT" immediateFlush="false">
<!--输出日志的格式 ${LOG_PATTERN} -->
<PatternLayout pattern="${LOG_PATTERN}" />
</console>
<!--File RollingFile 差不多一般用不到 可以忽略(适合临时测试用)-->
<!--File节点用来定义输出到指定位置的文件的appender name:指定Appender的名字 fileName:指定输出日志的目的文件带全路径的文件名 append="false" 是否在原有的日志文件中追加日志,如果为false则会清空-->
<File name="file" fileName="${LOG_HOME}/${PROJECT_NAME}/${LOG__NAME_TEMP}.log" append="false">
<PatternLayout pattern="${LOG_PATTERN}"/>
</File>
<!--RollingFile:日志输出到文件 name:定义这个RollingFile的名称 fileName:当前日志输出的文件名称-->
<!--filePattern:归档日志文件的文件名模式 ${LOG_HOME}/${PROJECT_NAME}/$${date:yyyy-MM-dd}/${LOG_NAME_INFO}-%d{yyyy-MM-dd HH}-%i.log.gz: 文件路径/项目名称/日期(年月日)/日志名称-年月日 小时-第一个.log.zip文件 -->
<RollingFile name="rollingFileInfo" fileName="${LOG_HOME}/${PROJECT_NAME}/${LOG_NAME_INFO}.log" filePattern="${LOG_HOME}/${PROJECT_NAME}/$${date:yyyy-MM-dd}/${LOG_NAME_INFO}-%d{yyyy-MM-dd-HH}-%i.log.zip">
<!--PatternLayout:日志内容输出格式-->
<PatternLayout pattern="${LOG_PATTERN}"/>
<!--Policies:触发策略决定何时执行备份 -->
<Policies>
<!--基于时间的滚动策略: interval="1"默认'1';这个配置需要和filePattern结合使用,filePattern日期格式精确到哪一位,interval也精确到哪一个单位 例如:%d{yyyy-MM-dd HH}-%i.log.zip 最小单位是小时 则interval="1" 则代表1小时-->
<!--modulate="true" 是否调整 interval 属性值,以便下次滚动发生在 interval 边界处-->
<!--maxRandomDelay: 滚动操作随机延迟的最长秒数。默认 0 表示无延迟。该设置在有多个应用同时滚动日志的服务器上很有用,可以扩宽滚动日志的的负载时间范围,避免某一个时刻由于滚动日志造成高 I/O 压力。-->
<TimeBasedTriggeringPolicy interval="1" modulate="true" maxRandomDelay="2"/>
<!--基于指定文件大小的滚动策略:每个文件最大100M -->
<SizeBasedTriggeringPolicy size="10M"/>
</Policies>
<filters>
<!--level="INFO" 匹配时接受,不匹配时丢弃-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</filters>
</RollingFile>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
<!--root 节点会输出所有等级的日志-->
<root level="INFO">
<!--表示root节点接收的日志信息 会以'consoleAppender'设置好的格式输出,但是他没有输出到文件-->
<appender-ref ref="consoleAppender"/>
</root>
<!--用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等-->
<!--name:带包指定包下输出的日志 例如:name="com.loong.test" 表示输出com.loong.test包下的日志-->
<!--level:表示日志输出的等级-->
<!--additivity:是否继承root节点,默认是true继承。(开发环境为了方便看控制台设置为true)
也就是说子Logger会在父Logger的appender里输出。若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出
例如:additivity="false" 则 name="com.loong.test" 的日志内容只会输出到 appender-ref ref="rollingFileInfo"中,而不会输出root节点中的appender-ref ref="consoleAppender"-->
<logger name="com.loong.test" level="INFO" additivity="true">
<appender-ref ref="rollingFileInfo"/>
</logger>
</loggers>
</configuration>
激活log4j2.xml
# 激活日志文件
logging:
config: classpath:log4j2.xml
至此log4j2同步日志就搭建好了
异步日志
步骤:
- 创建配置文件
- 设置select和丢弃策略
创建配置文件
在resource目录下创建
log4j2.component.properties
文件;配置好的log4j2.xml无需修改任何配置,并在文件中添加以下参数就可以了。本文介绍的异步日志是其实现的一种方式,若想使用可自行百度。
# 全局异步日志配置
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
# 异步队列满后执行策略(丢弃)
log4j2.asyncQueueFullPolicy=Discard
# 日志丢弃阈值(ERROR级别)
log4j2.discardThreshold=ERROR
拓展
异步日志原理
配置信息
配置名称 | 配置 | 默认值 | 描述 |
---|---|---|---|
RingBuffer槽位数量 | log4j2.asyncLoggerConfigRingBufferSize(同步&异步混合使用) | 256 * 1024 (garbage-free模式下默认为4*1024) | RingBuffer槽位数量,最小值为128,在首次使用的时候进行分配,并固定大小 |
异步日志等待策略 | log4j2.asyncLoggerConfigWaitStrategy(同步&异步混合使用) | Timeout (建议设置为Sleep) | Block:I/O线程使用锁和条件变量等待可用的日志事件,建议在CPU资源重要程度大于系统吞吐和延迟的情况下使用该种策略; Timeout:是Block策略的一个变体,可以确保如果线程没有被及时唤醒(awit())的话,线程也不会卡在那里,可以以一个较小的延迟(默认10ms)恢复; Sleep:是一种自旋策略,使用纳秒级别的自旋交叉执行Thread.yield()和LockSupport.parkNanos(),该种策略可以较好的平衡系统性能和CPU资源的使用率,并且对程序线程影响较小。 Yield:相较于Sleep省去了LockSupport.parkNanos(),而是不断执行Thread.yield()来让渡CPU使用权,但是会造成CPU一直处于较高负载,强烈不建议在生产环境使用该种策略 |
RingBuffer槽位数量 | log4j2.asyncLoggerRingBufferSize(纯异步) | 256 * 1024 (garbage-free模式下默认为4*1024) | RingBuffer槽位数量,最小值为128(2的指数),在首次使用的时候进行分配,并固定大小 |
异步日志等待策略 | log4j2.asyncLoggerWaitStrategy(纯异步) | Timeout (建议设置为Sleep) | Block:I/O线程使用锁和条件变量等待可用的日志事件,建议在CPU资源重要程度大于系统吞吐和延迟的情况下使用该种策略; Timeout:是Block策略的一个变体,可以确保如果线程没有被及时唤醒(awit())的话,线程也不会卡在那里,可以以一个较小的延迟(默认10ms)恢复; Sleep:是一种自旋策略,使用纳秒级别的自旋交叉执行Thread.yield()和LockSupport.parkNanos(),该种策略可以较好的平衡系统性能和CPU资源的使用率,并且对程序线程影响较小。 Yield:相较于Sleep省去了LockSupport.parkNanos(),而是不断执行Thread.yield()来让渡CPU使用权,但是会造成CPU一直处于较高负载,强烈不建议在生产环境使用该种策略 |
异步队列满后执行策略 | log4j2.asyncQueueFullPolicy | Default (强烈建议设置为Discard) | 当Appender的日志消费速度跟不上日志的生产速度,且队列已满时,指定如何处理尚未正常打印的日志事件,默认为阻塞(Default),强烈建议配置为丢弃(Discard) |
日志丢弃阈值 | log4j2.discardThreshold | INFO (强烈建议设置为ERROR) | 当Appender的消费速度跟不上日志记录的速度,且队列已满时,若log4j2.asyncQueueFullPolicy为Discard,该配置用于指定丢弃的日志事件级别(小于等于),默认为INFO级别(即将INFO,DEBUG,TRACE日志事件丢弃掉), 强烈建议设置为ERROR(FATAL虽然有些极端,但是也可以) |
异步日志超时时间 | log4j2.asyncLoggerSleepTimeNs | 100 | 异步日志等待策略为Sleep时,线程睡眠时间(单位:ns) |
异步日志重试次数 | log4j2.asyncLoggerRetries | 200 | 异步日志等待策略为Sleep时,自旋重试次数 |
队列满时是否将对RingBuffer的访问转为同步 | AsyncLogger.SynchronizeEnqueueWhenQueueFull | true | 当队列满时是否将对RingBuffer的访问转为同步,当Appender日志消费速度跟不上日志的生产速度,且队列已满时,通过限制对队列的访问,可以显著降低CPU资源的使用率,强烈建议使用该默认配置 |
配置方式
配置源 | 优先级(值越低优先级越高) | 描述 |
---|---|---|
Spring Boot Properties | -100 | Spring Boot日志配置,需要有【log4j-spring】模块支持 |
System Properties | 0 | 在类路径下添加log4j2.system.properties(推荐) |
Environment Variables | 100 | 使用环境变量进行日志配置,注意该系列配置均以LOG4J_作为前缀 |
log4j2.component.properties file | 200 | 在类路径下添加log4j2.component.properties(其实是【System Properties】的一种配置方式,但是具有较低的优先级,一般用作默认配置) |
以上内容均参考自Log4j2官方文档:Log4j – Configuring Log4j 2 (apache.org)
总结
- 在日志可以丢弃的情况下,推荐使用log4j2.asyncQueueFullPolicy=Discard 和log4j2.discardThreshold=ERROR的组合配置;
- 不要在生产环境使用可以直接与中间件交互的的Appender,如KafkaAppender。此类Appender一般都会有ack机制,与直接打印到日志文件不同的地方便是需要进行网络交互,如果中间件性能出现问题或者网络出现抖动,那么同样也会造成日志阻塞(话分两头,如果必须要使用KafkaAppender,那么可以考虑将syncSend设置为【false】,该配置的作用是指定Appender是否需要等待Kafka Server的ack,但是需要注意的是该开关配置需要Log4j2版本为2.8+);
- Appender的【immediateFlush】设置为【false】,使用批量日志写入的方式,避免频繁的执行写磁盘操作。
此类Appender一般都会有ack机制,与直接打印到日志文件不同的地方便是需要进行网络交互,如果中间件性能出现问题或者网络出现抖动,那么同样也会造成日志阻塞(话分两头,如果必须要使用KafkaAppender,那么可以考虑将syncSend设置为【false】,该配置的作用是指定Appender是否需要等待Kafka Server的ack,但是需要注意的是该开关配置需要Log4j2版本为2.8+); - Appender的【immediateFlush】设置为【false】,使用批量日志写入的方式,避免频繁的执行写磁盘操作。
- 上述三个配置可以保证在极端情况下日志的打印不会成为系统瓶颈,喂系统服下一颗“定心丸”。
参考文章:https://zhuanlan.zhihu.com/p/680317697