log4j原理与实战
前言
从我刚开始学习Java语言时,就知道java日志框架log4j很实用,但是这个框架一直都不是面试考点,并且System.out.println()能解决的事,为什么要这么麻烦地引用第三方jar包来做呢?随着我对Java使用的场景越来越多,越来越发现System.out.println()功能太弱了。本文档就我自己的感悟,记录我觉得曾经难以理解,对log4j望而却步的技术点。
备注
我知道java自带了日志模块java.util.logging,但是现在风评更好的是第三方框架log4j,因此我放弃学习java.util.logging,本文档提到的日志框架默认就是log4j。
场景
以下是我转战log4j的场景:
- 实时作业累计产生的日志太多,经常几天后flink日志文件就会庞大到打不开,因此我看不到我最想看的那一部分日志信息,最开始我把这部分日志存到redis中,很麻烦。是不是可以使用log4j把这部分日志打印到文件中进行保存?
- System.out.println()方法只能打印内容,我希望知道打印内容时的时间,是哪个类/方法打印了该信息,这只能通过log4j实现。
- 如果系统产生的json记录过多,若直接存储到一个文件中,文件会非常庞大。log4j可以设置每隔N条记录,存储成一个新文件,将这些json记录存储成多个小文件方便后续读取。
引入log4j依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
配置文件
配置文件是log4j的灵魂,定义了log4j执行哪些行为。一旦配置文件出错,log4j就会警告,这时候需要检查其语法和格式:
log4j:WARN No appenders could be found for logger
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
注意:可以使用api配置log4j,但是代码侵入性太强,还是使用配置文件更好。
位置
一般而言,配置文件需要放到resources目录下,以log4j.properties命名。
组件
log4j由三个组件构成:
- logger:日志级别。有时候我希望输出调试级别的日志、普通日志、警告级别的日志、发生的错误日志、重大错误级别的日志,它们分别对应log4j中的DEBUG、INFO、WARN、ERROR和FATAL,使用Logger类的debug()、info()、warn()、error()、fatal()方法即可打印不同级别的日志。分这几种级别的目的是可以分别打印或者存储不同级别的日志,这样只需要简单地配置一下,log4j.properties,就可以轻松满足多种需求。
- appender:日志输出的目的地。有时候我希望将日志打印到终端上,同时存储到文件中。那么可以在log4j中定义两个appender,配置一个appender打印到终端上,配置另一个appender存储到文件中即可。
- layout:日志格式。有时候希望在日志前面加上时间和类名/方法名等前缀,可以在每个appender下通过layout属性定义不同的日志格式。
上述三个组件在log4j.properties中必须定义,缺一不可,否则报警,不打印任何信息。
语法
以下是log4j模版以及解析:
# 定义多个appender,指定每个appender输出日志的级别。
# appenderName只是一个名字,后续会定义每个appenderName打印什么级别的日志,打印到哪里,通过什么格式打印,appenderName可以由自己随意命名。
# level表示每个appenderName打印的日志级别。级别优先级为DEBUG < INFO < WARN < ERROR < FATAL。当level设置为INFO时,appender会打印INFO、WARN、ERROR和FATAL级别的日志;当level设置为ERROR时,appender会打印ERROR和FATAL级别的日志。
log4j.rootLogger = [level], appenderName1, appenderName2, …
# 开始配置log4j.rootLogger定义的appender。
# 配置已经定义好了的appenderName1。
# class表示appenderName1输出日志的实现类,每个类定义了appender的行为,例如org.apache.log4j.ConsoleAppender类表示将日志打印到控制台,而
org.apache.log4j.FileAppender表示将日志输出到文件。还有很多其他实现类定义了appender行为,可以自行查阅其他资料。
log4j.appender.appenderName1 = [class]
# level:设置appenderName1日志输出级别,会和rootLogger设置的level进行比较,优先级最高的level生效
log4j.appender.appenderName1.Threshold=[level]
# target:用于给控制台的日志上色,System.out输出黑色,System.err输出红色。
log4j.appender.appenderName1.target= [target]
# class表示appenderName1的日志格式实现类,常用的类是org.apache.log4j.PatternLayout,它可以自定义输出的内容
log4j.appender.stdout.layout = [class]
# pattern 用于指定日志输出格式。例如,[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n最终输出,其中%-5p表示左对齐输出报警级别,它们占固定占5个字符,不足5个字符以空格替代(默认是右对齐,加上-后左对齐),%d{yyyy-MM-dd HH:mm:ss,SSS} 表示时间格式,method:是自定义字符串,%l表示输出类、方法名、行号信息,%n表示换行,%m表示日志内容。例如:[INFO ] 2021-08-05 21:21:46,591 method:Entry.main(Entry.java:9)
log4j.appender.stdout.layout.ConversionPattern = [pattern]
实例
下面是针对模版所写的常用的日志配置文件,并对其做出解释:
#定义三个appender,分别是stdout,D,E,它们只处理info级别以上的日志
log4j.rootLogger = info,stdout,D,E
# stdout这个appender会将日志输出到控制台
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
# 控制台中打印的日志字体为黑色
log4j.appender.stdout.Target = System.out
# 自定义日志格式
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p][%d][%t][%l]: %m%n
# D这个appender会将日志按天分别存储
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
# 日志会存储到当前目录下的logs目录,以log.log保存
log4j.appender.D.File = logsD/log.log
# 追加方式,true表示当前程序运行的日志会append上一次运行的日志;false表示当前程序运行的日志会覆盖上一次运行的日志
log4j.appender.D.Append = true
# 新的一天的日志文件名会带上'.年-月-日-时-分'这个后缀
log4j.appender.logfile.DataPattern='.'yyyy-MM-dd-HH-mm
# 和rootLogger的日志级别去优先级最高,日志级别为info
log4j.appender.D.Threshold = DEBUG
# 自定义日志格式
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = [%-5p][%d][%t][%l]: %m%n
# R这个appender将日志保存到文件中
log4j.appender.R=org.apache.log4j.RollingFileAppender
# 和rootLogger的日志级别去优先级最高,日志级别为info
log4j.appender.R.Threshold=DEBUG
# 日志存储位置是当前目录的log目录,日志文件是test.log
log4j.appender.R.File=log/test.log
# 自定义日志格式
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=[%-5p][%d][%t][%l]: %m%n
# 每个日志最大保存的日志内容大小时1KB,超过1KB,新建一个日志文件。
log4j.appender.R.MaxFileSize=1KB
# 最多能保存10个新日志文件,超过这个数量就覆盖掉最旧的日志文件
log4j.appender.R.MaxBackupIndex=10
API使用
通常,在创建log4j日志对象时,需要传人class对象:
private static Logger logger = Logger.getLogger(Entry.class);
它的作用是可以在打印日志中指出所在的类及其方法,如下所示,日志打印了类和方法:
[INFO ] 2021-08-06 12:04:21,412 method:Entry.main(Entry.java:20)
content
RollingFileAppender实战
需求
将日志存储到文件中,为了不使文件过大,当文件大小达到指定大小时,创建新的文件存储日志,系统最多N个新日志文件。
思考
使用org.apache.log4j.RollingFileAppender
这个Appender类存储日志文件,使用MaxFileSize
参数设置每个文件的大小阈值,使用MaxBackupIndex
这个参数设置保存的文件数。但是我担心如果一条日志append到旧文件时,如果文件大小超过MaxFileSize
,那是否将日志切分成两部分以避免文件大小超过MaxFileSize
,这会破坏这条日志数据,因此我实验了一下:
Java类
Java类每次向写日志文件时,每条日志的大小时2048Byte。
import org.apache.log4j.Logger;
import java.util.Random;
public class Entry {
private static Logger logger = Logger.getLogger(Entry.class);
public static void main(String[] args) {
while(true) {
char[] charArr = new char[2048];
char[] chrList = {'a','b','c','d','e','f','g','h','i','j'};
Random random = new Random();
for(int i = 0; i < 2048; ++i){
int index = random.nextInt(10);
charArr[i] = chrList[index];
}
String str = new String(charArr);
logger.debug(str);
logger.info(str);
logger.warn(str);
logger.error(str);
logger.fatal(str);
}
}
}
配置文件
为了验证存储日志时,是否会为了满足MaxFileSize
将某条日志切分成两部分,设置文件大小是1KB。这样如果日志文件是2KB时,说明log4j不会破坏那条日志;如果日志文件是1KB,说明log4j破坏了那条日志。
log4j.rootLogger = info,R
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.Threshold=DEBUG
log4j.appender.R.File=log/test.log
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=[%-5p][%d][%t][%l]: %m%n
log4j.appender.R.MaxFileSize=1KB
log4j.appender.R.MaxBackupIndex=10
结果
最终在当前目录下产生了10个新日志文件,每个日志文件是2079Byte=2048Byte(日志本身)+31Byte(前缀):
yuliang@yuliangdeMacBook-Pro log % ls -al
total 80
drwxr-xr-x 12 yuliang staff 384 Aug 6 11:41 .
drwxr-xr-x 8 yuliang staff 256 Aug 6 11:41 ..
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.10
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.2
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.3
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.4
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.5
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.6
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.7
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.8
-rw-r--r-- 1 yuliang staff 2079 Aug 6 11:41 test.log.9
由于实际的日志文件加入新的一条日志后允许超过MaxFileSize
,因此log4j不会破坏日志数据,可放心使用。
文件切割方式
从test.log文件可以看到所有日志会首先写到test.log中,当超过MaxFileSize
时,将文件进行分割,最旧的日志转移到新文件test.log.1中,此时test.log中只剩最新的日志了;过一段时间test.log文件大小又超过MaxFileSize
时,再次将文件进行切割,将旧文件转移到test.log.1中,原来属于test.log.1文件的日志转移到新文件test.log.2中。因此不同文件日志从旧到新的顺序依次为:test.log.10、test.log.9、test.log.8、...、test.log.1。
LevelRangeFilter实战
需求
info和warn打印成黑色字体,error和fatal打印成红色字体。
分析
如果按照上面的配置,设置两个appender,一个的Threshold为INFO,一个的Threshold为ERROR,那么ERROR级别的日志会打印两次,那么这就需要使用log4j的新特性:filter了。
配置文件
### set log levels ###
log4j.rootLogger = info,stdout,stderr
### info -> black
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.target = System.out
log4j.appender.stdout.Threshold = DEBUG
log4j.appender.stdout.filter.1=org.apache.log4j.varia.LevelRangeFilter
log4j.appender.stdout.filter.1.LevelMax = WARN
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p][%d][%t][%l]: %m%n
### error -> red
log4j.appender.stderr = org.apache.log4j.ConsoleAppender
log4j.appender.stderr.target = System.err
log4j.appender.stderr.Threshold = ERROR
log4j.appender.stderr.filter.1=org.apache.log4j.varia.LevelRangeFilter
log4j.appender.stderr.filter.1.LevelMin = ERROR
log4j.appender.stderr.layout = org.apache.log4j.PatternLayout
log4j.appender.stderr.layout.ConversionPattern = [%-5p][%d][%t][%l]: %m%n
解析
上述配置中,增加了filter相关属性。filter负责对当钱appender处理的日志进行过滤。filter模版为:
# ID表示不同的Filter代号,即一个appender可以设置多个filter。每个filter需要设置一个实现类
log4j.appender.appenderName.filter.ID=fully.qualified.name.of.filter.class
# 通过设置实现类的字段控制该filter的过滤行为
log4j.appender.appenderName.filter.ID.option1=value1
...
log4j.appender.appenderName.filter.ID.optionN=valueN
在本案例中,需要使用org.apache.log4j.varia包下的LevelRangeFilter类,包下还有其他类,可以自行研究。
log4j.appender.stdout.filter.1=org.apache.log4j.varia.LevelRangeFilter
# 设置stdout这个appender处理的日志的最高级别为WARN
log4j.appender.stdout.filter.1.LevelMax = WARN
# 设置stderr这个appender处理的日志的最低级别是ERROR
log4j.appender.stderr.filter.1.LevelMin = ERROR
输出:
代码调用了5次方法,分别是debug()、info()、warn()、error()、fatal(),最终打印了四条信息:
说明没有冗余输出,filter配置正确。
扩展
1.Layout扩展:https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/PatternLayout.html