Log4j2实践

1 日志框架

(1) slf4j

一种日志框架规范、标准和接口,不是具体实现。用户通过slf4j作为代理,间接使用具体的日志框架。可以在不更改项目代码的前提下,更换具体日志框架。

(2) log4j2、log4j和logback

具体的日志框架,可以与slf4j搭配使用。

其中,log4j2相比log4j具有更好的吞吐量和性能,解决了一些死锁bug,并新增了无锁异步等技术。

2 Spring Boot项目配置

以下适用于版本2.2.2

(1)依赖处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<!--排除logback-->
<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-web</artifactId>
<exclusions>
<!--排除logback-->
<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-web-services</artifactId>
<exclusions>
<!--排除logback-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
<exclusions>
<!--排除logback-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
<exclusions>
<!--排除logback-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<exclusions>
<!--排除logback-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<!--排除logback-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

...

<!--log4j2 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

(2) 配置

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!-- monitorInterval配置成一个正整数,则每隔这么久的时间(秒),log4j2会刷新一次配置。如果不配置则不会动态刷新 -->
<Configuration status="INFO" monitorInterval="30">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->

<!--变量配置-->
<Properties>
<!-- 定义日志存储的路径 -->
<!-- <property name="FILE_PATH" value="更换为你的日志路径" />-->
<!-- <property name="FILE_NAME" value="更换为你的项目名" />-->
<Property name="LOG_DIR">logs</Property>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<!-- %logger{36} 表示 Logger 名字最长36个字符 -->
<Property name="LOG_PATTERN">%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Property>
</Properties>
<!-- 先定义所有的appender -->
<Appenders>
<!-- 这个输出控制台的配置 -->
<Console name="CONSOLE" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Console>

<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
<!-- <File name="FILE_LOG" fileName="${LOG_DIR}/test.log" append="false">-->
<!-- <PatternLayout pattern="${LOG_PATTERN}"/>-->
<!-- </File>-->

<!-- 系统日志,可以作为root logger的appender,供打印一些中间件的日志 -->
<RollingRandomAccessFile name="SYS_APPENDER" fileName="${LOG_DIR}/server.log"
filePattern="${LOG_DIR}/server.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="200MB" />
<!-- modulate属性是指从启动时间开始算5秒,还是从0秒开始算5秒,运行一下就明白了。
modulate: true(默认值) // 会从启动时间开始算 5秒
modulate: false // 从 0秒开始算-->
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<!-- max=6标识一小时内最多产生6个日志文件 -->
<DefaultRolloverStrategy max="6">
<!-- 对于指定的路径下的指定后缀的文件,只保留1天的日志文件,那么最多会有24小时*6个日志文件 -->
<Delete basePath="${LOG_DIR}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="1d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>

<!-- 应用info日志 -->
<RollingRandomAccessFile name="INFO_APPENDER" fileName="${LOG_DIR}/info.log"
filePattern="${LOG_DIR}/info.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<Filters>
<!-- 当前appender只打印info日志,warn及以上日志忽略,由后面的appender决定是否需要打印 -->
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- max=20标识一小时内最多产生20个日志文件 -->
<DefaultRolloverStrategy max="20">
<!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3天*24小时*20个日志文件 -->
<!-- 注意应用需要根据业务需求和磁盘大小评估需要保留的日志个数,对于500M的日志文件来说,要根据应用日志的情况,观察单个日志压缩后文件大小,并计算总大小需要的空间 -->
<Delete basePath="${LOG_DIR}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>

<!-- 应用错误日志 -->
<RollingRandomAccessFile name="ERROR_APPENDER" fileName="${LOG_DIR}/error.log"
filePattern="${LOG_DIR}/error.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<Filters>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- max=10标识一小时内最多产生10个日志文件 -->
<DefaultRolloverStrategy max="10">
<!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3*24小时*10个日志文件 -->
<Delete basePath="${LOG_DIR}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
</Appenders>

<!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
<!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
<Loggers>
<!-- root是默认的logger,也就是公共的logger,供记录一些不常打印的系统参数或者其他组件参数 -->
<AsyncRoot level="INFO">
<AppenderRef ref="CONSOLE" />
<AppenderRef ref="SYS_APPENDER" />
</AsyncRoot>
<!-- 常打印的应用日志,建议独立配置,并采用异步模式。name根据实际的包名修改,生产环境中additivity建议设置为false以避免在root logger中重复打印 -->
<!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
<!-- 思考:若是additivity设为false,指定了包路径,符合条件的日志只会在配置的APPENDER中打印,不会再ROOT中打印。因此可以将不同包路径的日志分流 -->
<AsyncLogger name="com.hopefulnick" level="INFO" includeLocation="false" additivity="false">
<!-- <AppenderRef ref="FILE_LOG" />-->
<AppenderRef ref="INFO_APPENDER" />
<AppenderRef ref="ERROR_APPENDER" />
</AsyncLogger>
</Loggers>
</Configuration>

日志滚动策略

容量计算公式

1
日志空间需求=日志滚动阈值(例如500M)+日志留存个数*日志滚动阈值*1/压缩比(对于gz压缩比一般会是几十,具体根据应用日志压缩后计算)

3 日志调优

  • 在Configuration中添加monitorInterval,以支持动态刷新
  • 使用异步日志,而不是同步日志,可以是混合异步也可以是全局异步
  • 不推荐配置AsyncAppender,如果需要混合异步,使用AsyncLogger
  • PatternLayout不要使用%L、%C、%method等含有“位置信息”的配置项,非常影响性能。同时logger配置中需要加上inclueLocation=”false”,这样即使配置了位置信息也会被忽略
  • 使用RollingRandomAccessFile做为appender
  • 基于大小和时间的双重文件滚动策略,并配合压缩

4 踩坑指南

(1) 输出日志格式不对

  • 确认配置文件是否生效,否则移动位置或将当前位置加入到资源路径中。
  • 检查是否将spring boot库中的所有logback屏蔽

(2) NoClassDefFoundError: com/lmax/disruptor/EventFactory

1
2
3
4
5
6
7
<!-- 异步日志必须 -->
<!-- https://mvnrepository.com/artifact/com.lmax/disruptor -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>

(3) 不同的日志输出到不同的文件

检查<Loggers>是否正确

(4) IDE更新

IDE更新后需要安装Lombok支持

参考资料

Custom Log Configuration

Springboot整合log4j2日志全解

日志框架比较(slf4j、log4j、logback、log4j2 )

SpringBoot中集成Log4j2以及Log4j2的xml配置

Log4j2最佳实践

浅谈Log4j2日志框架及使用

SpringBoot中集成Log4j2以及Log4j2的xml配置

Log4j2 简明教程

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!-- monitorInterval配置成一个正整数,则每隔这么久的时间(秒),log4j2会刷新一次配置。如果不配置则不会动态刷新 -->
<Configuration status="INFO" monitorInterval="30">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->

<!--变量配置-->
<Properties>
<!-- 定义日志存储的路径 -->
<!-- <property name="FILE_PATH" value="更换为你的日志路径" />-->
<!-- <property name="FILE_NAME" value="更换为你的项目名" />-->
<Property name="LOG_DIR">logs</Property>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<!-- %logger{36} 表示 Logger 名字最长36个字符 -->
<Property name="LOG_PATTERN">%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Property>
</Properties>
<!-- 先定义所有的appender -->
<Appenders>
<!-- 这个输出控制台的配置 -->
<Console name="CONSOLE" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Console>

<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
<!-- <File name="FILE_LOG" fileName="${LOG_DIR}/test.log" append="false">-->
<!-- <PatternLayout pattern="${LOG_PATTERN}"/>-->
<!-- </File>-->

<!-- 系统日志,可以作为root logger的appender,供打印一些中间件的日志 -->
<RollingRandomAccessFile name="SYS_APPENDER" fileName="${LOG_DIR}/server.log"
filePattern="${LOG_DIR}/server.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="200MB" />
<!-- modulate属性是指从启动时间开始算5秒,还是从0秒开始算5秒,运行一下就明白了。
modulate: true(默认值) // 会从启动时间开始算 5秒
modulate: false // 从 0秒开始算-->
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<!-- max=6标识一小时内最多产生6个日志文件 -->
<DefaultRolloverStrategy max="6">
<!-- 对于指定的路径下的指定后缀的文件,只保留1天的日志文件,那么最多会有24小时*6个日志文件 -->
<Delete basePath="${LOG_DIR}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="1d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>

<!-- 应用info日志 -->
<RollingRandomAccessFile name="INFO_APPENDER" fileName="${LOG_DIR}/info.log"
filePattern="${LOG_DIR}/info.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<Filters>
<!-- 当前appender只打印info日志,warn及以上日志忽略,由后面的appender决定是否需要打印 -->
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL" />
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- max=20标识一小时内最多产生20个日志文件 -->
<DefaultRolloverStrategy max="20">
<!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3天*24小时*20个日志文件 -->
<!-- 注意应用需要根据业务需求和磁盘大小评估需要保留的日志个数,对于500M的日志文件来说,要根据应用日志的情况,观察单个日志压缩后文件大小,并计算总大小需要的空间 -->
<Delete basePath="${LOG_DIR}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>

<!-- 应用错误日志 -->
<RollingRandomAccessFile name="ERROR_APPENDER" fileName="${LOG_DIR}/error.log"
filePattern="${LOG_DIR}/error.log.%d{yyyyMMddHH}.%i.gz">
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="500MB" />
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
<Filters>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<!-- max=10标识一小时内最多产生10个日志文件 -->
<DefaultRolloverStrategy max="10">
<!-- 对于指定的路径下的指定后缀的文件,只保留3天的日志文件,那么最多会有3*24小时*10个日志文件 -->
<Delete basePath="${LOG_DIR}" maxDepth="1">
<IfFileName glob="*.gz" />
<IfLastModified age="3d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
</Appenders>

<!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
<!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
<Loggers>
<!-- root是默认的logger,也就是公共的logger,供记录一些不常打印的系统参数或者其他组件参数 -->
<AsyncRoot level="INFO">
<AppenderRef ref="CONSOLE" />
<AppenderRef ref="SYS_APPENDER" />
</AsyncRoot>
<!-- 常打印的应用日志,建议独立配置,并采用异步模式。name根据实际的包名修改,生产环境中additivity建议设置为false以避免在root logger中重复打印 -->
<!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
<!-- 思考:若是additivity设为false,指定了包路径,符合条件的日志只会在配置的APPENDER中打印,不会再ROOT中打印。因此可以将不同包路径的日志分流 -->
<AsyncLogger name="com.hopefulnick" level="INFO" includeLocation="false" additivity="false">
<!-- <AppenderRef ref="FILE_LOG" />-->
<AppenderRef ref="INFO_APPENDER" />
<AppenderRef ref="ERROR_APPENDER" />
</AsyncLogger>
</Loggers>
</Configuration>