log4j、log4j2、logback日志关系

1、Apache Log4j 是一个老的日志框架,并且是多年来最受欢迎的日志框架。2015 年 8 月 5 日,该项目管理委员会宣布 Log4j 1.x 已达到使用寿命。 建议用户使用 Log4j 1 升级到 Apache Log4j2。

2、logback 是由 log4j 创始人设计的又一个开源日志组件,作为流行的 log4j 项目的后续版本,从而替代 log4j。原生实现了SLF4J。

3、Apache Log4j 2是对 Log4j 的升级,它比其前身 Log4j 1.x 提供了重大改进,并提供了 Logback 中可用的许多改进,同时修复了 Logback 架构中的一些固有问题。

log4j2包含基于lmax disruptor库的下一代异步记录器(Asynchronous Loggers)。在多线程场景中,异步日志记录器的吞吐量是log4j1.x和Logback的18倍,延迟也要低几个数量级。log4j2的性能明显优于log4j1.x、Logback和java.util.logging,尤其是在多线程应用程序中。

4,队列的选择对于峰值吞吐量非常重要。log4j2的异步记录器使用无锁数据结构(disruptor),而Logback、log4j1.2和log4j2的异步附加器使用ArrayBlockingQueue。对于阻塞队列,多线程应用程序在尝试将日志事件排队时经常遇到锁争用

性能比较:http://logging.apache.org/log4j/2.x/performance.html

1,异步日志吞吐量

log4j漏洞怎么发现的(002-slf4jlog4j使用及源码分析)(1)

Y轴:单位msg/sec , 每秒消息数。

X轴:线程数。

说明:随着线程数的增加,log4j2 使用全部异步loggers ,性能达到每秒处理1800万消息。

2,异步日志记录响应时间

log4j漏洞怎么发现的(002-slf4jlog4j使用及源码分析)(2)

Y轴:单位:延迟毫秒数

X轴:响应时间百分比。

下面以springboot项目来介绍日志用法

新建一个项目,只引入spring-boot-starter-web stater.

1,Log4j 用法

说明:虽然新项目一般都不会用log4j了,但是一些老项目还在使用。学习也是必要的。

根据前面介绍:SpringBoot选用 SLF4j和logback默认实现;

(1)使用slf4j log4j的方式,需要排除默认logback日志依赖,引入slf4j log4j依赖.

jul-to-slf4j,jcl-over-slf4j 包是替换其他日志框架,使使用jul,jcl日志框架的类也通过slf4jAPI 输出日志,由log4j 来实现。

log4j漏洞怎么发现的(002-slf4jlog4j使用及源码分析)(3)

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <artifactId>spring-boot-starter-logging</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.12</version> <!--包含了slf4j log4j包--> </dependency>

(2)在resources目录下创建log4j.properties文件

### 设置### log4j.rootLogger = debug,console,File1,File2 ### 输出信息到控制抬 ### log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n ### 输出DEBUG 级别以上的日志到D:/logs/debug.log ### # 日志记录到多个文件 log4j.appender.File1 = org.apache.log4j.RollingFileAppender # 日志文件的名称 log4j.appender.File1.File = D:/logs/debug.log # 默认设置为true,这意味着记录的信息被附加到同一文件的末尾 log4j.appender.File1.Append = true # 标志的默认设置为true,这意味着输出流的文件被刷新,在每个追加操作 log4j.appender.File1.ImmediateFlush=true log4j.appender.File1.Threshold = DEBUG # 文件的回滚临界尺寸。默认值是10MB # 此示例配置说明每个日志文件的最大允许大小为5MB。当超过最大尺寸,新的日志文件将被创建 # 因为maxBackupIndex被定义为2,当第二个日志文件达到最大值,第一个日志文件将被删除, # 之后所有的日志信息将被回滚到第一个日志文件。 log4j.appender.File1.MaxFileSize = 5MB # 此属性表示要创建的备份文件的数量。默认值是1 log4j.appender.File1.MaxBackupIndex = 10 log4j.appender.File1.Encoding = UTF-8 log4j.appender.File1.layout = org.apache.log4j.PatternLayout log4j.appender.File1.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n ### 输出ERROR 级别以上的日志到D:/logs/error.log ### # 每天生成一个日志文件 log4j.appender.File2 = org.apache.log4j.DailyRollingFileAppender log4j.appender.File2.File =D:/logs/error log4j.appender.File2.Append = true log4j.appender.File2.DatePattern = '.'yyyy-MM-dd-HH-mm'.log' log4j.appender.File2.Threshold = ERROR log4j.appender.File2.layout = org.apache.log4j.PatternLayout log4j.appender.File2.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n log4j.logger.com.example.springboot=debug,myLog log4j.additivity.com.example.springboot=false log4j.appender.myLog=org.apache.log4j.ConsoleAppender log4j.appender.myLog.Target=System.out log4j.appender.myLog.Threshold=debug log4j.appender.myLog.layout=org.apache.log4j.PatternLayout log4j.appender.myLog.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss:SSS}] %5p %c{1}:%L - %m%n

(3)代码中使用

public class Log4JTest { private static final Logger logger = LoggerFactory.getLogger(Log4JTest.class); public static void main(String[] args) { // 记录debug级别的信息 logger.debug("This is debug message."); // 记录info级别的信息 logger.info("This is info message."); // 记录error级别的信息 logger.error("This is error message."); } }

Log4J框架的不同组件的虚拟图:

log4j漏洞怎么发现的(002-slf4jlog4j使用及源码分析)(4)

Filter对象:

可以过滤日志级别,比如只想输出INFO,不想输出WARN,ERROR到文件。

一个appender对象可以有与之关联的几个Filter对象。

日志管理:

日志管理对象管理的日志框架。它负责从一个系统级的配置文件或配置类读取初始配置参数。比如从resources目录下读取log4j.properties

log4j.properties文件是一个键 - 值对保存 log4j 配置属性文件。默认情况下,日志管理在CLASSPATH 查找一个名为 log4j.properties 的文件。

Log4j由三个重要的组件构成:

日志信息的优先级从高到低有ERROR、WARN、 INFO、DEBUG,分别用来指定这条日志信息的重要程度;日志信息的输出目的地指定了日志将打印到控制台还是文件中;而输出格式则控制了日志信息的显示内容。

1.1. 配置根Logger,其语法为:

log4j.rootLogger = [ level ] , appenderName, appenderName, …

其中,level 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者您定义的级别。

Log4j建议只使用四个级别,优先级从高到低分别是ERROR、WARN、INFO、DEBUG。

比如:指定level = INFO , INFO级别以下日志不输出,输出自身及以上日志。appenderName就是指把日志信息输出到哪个地方,比如上面定义了输出到控制台(console),文件1(File1),文件2(File2)

定义非根Logger(很有用)

log4j.logger.loggerName1 = [ level ], appendName1,…appendNameN

比如我们只想输出程序中com.example.springboot包下的INFO日志,可以配置

log4j.logger.com.example.springboot=info 该级别日志优先级大于根level (如果根level=debug或者 level = error,以info为准)

当然也可以自定义输出appender为myLog

log4j.logger.com.example.springboot=info,myLog log4j.additivity.com.example.springboot=false log4j.appender.myLog=org.apache.log4j.ConsoleAppender log4j.appender.myLog.Target=System.out log4j.appender.myLog.Threshold=INFO log4j.appender.myLog.layout=org.apache.log4j.PatternLayout log4j.appender.myLog.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ss:SSS}] %5p %c{1}:%L - %m%n # 默认情况下子Logger会继承父Logger的appender,也就是说子Logger会在父Logger的appender里输出。 #若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出。

1.2. 配置文件的输出目的地Appender,一般,配置代码的格式如下

log4j.appender.appenderName = fully.qualified.name.of.appender.class log4j.appender.appenderName.[option] = [value]

log = /usr/home/log4j log4j.appender.FILE.File=${log}/log.out 需要注意的是log4j支持UNIX风格的变量替换,如 ${variableName}.

其中,Log4j提供的appender有以下几种:

Appender默认配置:

log4j漏洞怎么发现的(002-slf4jlog4j使用及源码分析)(5)

1.3.配置日志信息的格式(布局),其语法为

log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class log4j.appender.appenderName.layout.option1 = value1 log4j.appender.appenderName.layout.[option] = [value]

其中,Log4j提供的layout有以下几种:

# 上面log4j.properties的console 的布局就是PatternLayout log4j.appender.console.layout = org.apache.log4j.PatternLayout # layout输出格式 log4j.appender.console.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n

Log4J的printf函数的打印格式格式化日志信息,打印的参数含义如下:

[%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n 输出类似:[2020-12-11 22:40:02:803] [INFO] [main]- com.example.springboot.Log4JTest.main(Log4JTest.java:18) - This is info message.

1.4.日志记录到多个文件

log4j.appender.File1 = org.apache.log4j.RollingFileAppender # 日志文件的名称 log4j.appender.File1.File = D:/logs/debug.log # 默认设置为true,这意味着记录的信息被附加到同一文件的末尾 log4j.appender.File1.Append = true # 标志的默认设置为true,这意味着输出流的文件被刷新,在每个追加操作 log4j.appender.File1.ImmediateFlush=true log4j.appender.File1.Threshold = DEBUG # 文件的回滚临界尺寸。默认值是10MB # 此示例配置说明每个日志文件的最大允许大小为5MB。当超过最大尺寸,新的日志文件将被创建 # 因为maxBackupIndex被定义为2,当第二个日志文件达到最大值,第一个日志文件将被删除, # 之后所有的日志信息将被回滚到第一个日志文件。 log4j.appender.File1.MaxFileSize=5 # 此属性表示要创建的备份文件的数量。默认值是1 log4j.appender.File1.MaxBackupIndex=2 log4j.appender.File1.Encoding = UTF-8 log4j.appender.File1.layout = org.apache.log4j.PatternLayout log4j.appender.File1.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n

1.5. 每天生成日志文件

DatePattern控制使用下列滚动的时间表方式之一:

log4j漏洞怎么发现的(002-slf4jlog4j使用及源码分析)(6)

# 每天生成一个日志文件 log4j.appender.File2 = org.apache.log4j.DailyRollingFileAppender log4j.appender.File2.File =D:/logs/error log4j.appender.File2.Append = true log4j.appender.File2.DatePattern = '.'yyyy-MM-dd'.log' log4j.appender.File2.Threshold = ERROR log4j.appender.File2.layout = org.apache.log4j.PatternLayout log4j.appender.File2.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n

学到这里,思考一下?如何配置只输出INFO 信息到指定文件,也就是该文件中不包含WARN,ERROR级别日志?

log4j提供了Appender的Threshold属性,可以设置该appender输出什么级别的日志。

log4j.appender.File1.Threshold=INFO 表示打印大于、等于该级别的日志,会输出了info、warn和error级别的日志

解决方案,这时要用到Filter

log4j.appender.File2 = org.apache.log4j.DailyRollingFileAppender log4j.appender.File2.File =D:/logs/info.log log4j.appender.File2.Threshold = INFO log4j.appender.File2.layout = org.apache.log4j.PatternLayout log4j.appender.File2.layout.ConversionPattern = [%d{yyyy-MM-dd HH:mm:ss:SSS}] [%p] [%t]- %l - %m%n #通过Filter过滤设置最大,最小日志级别 log4j.appender.File2.filter.infoFilter = org.apache.log4j.varia.LevelRangeFilter log4j.appender.File2.filter.infoFilter.LevelMin = INFO log4j.appender.File2.filter.infoFilter.LevelMax = INFO

另一种解决方法:重写RollingFileAppender类中的isAsSevereAsThreshold方法:

// 默认实现 ,如果没有设置threshold属性则全部打印,否则打印大于等于threshold属性的日志。

public boolean isAsSevereAsThreshold(Priority priority) { return this.threshold == null || priority.isGreaterOrEqual(this.threshold); } public class MyInfoLog4jAppender extends RollingFileAppender { @Override public boolean isAsSevereAsThreshold(Priority priority) { return priority != null && this.threshold == null && priority.isGreaterOrEqual(this.threshold) && this.getThreshold().isGreaterOrEqual(priority); } }

在log4j.properties文件中修改log4j.appender.File1=com.xxx.MyInfoLog4jAppender即可。

rootcategory与rootlogger区别?

在配置文件有时候会看到这样写法:

log4j.rootLogger = debug,console,File1,File2 log4j.rootcategory= debug,console,File1,File2

通过官网查证:rootCategory 是废弃API , 建议使用rootLogger。为了保持兼容:Logger is a subclass of Category。所以请使用log4j.rootLogger。

到这里log4j的基本用法和常见问题就讲完了。如果遇到新的问题,可自行google.

接下来讲讲源码:

<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.12</version> </dependency> 以1.7.12版本讲解。 心得:学习源码的方式,就是通过一个小demo,debug一下。 public class Slf4jTest { private static Logger logger = LoggerFactory.getLogger(Slf4jTest.class); public static void main(String[] args){ if(logger.isDebugEnabled()){ logger.debug("slf4j-log4j debug message"); } if(logger.isInfoEnabled()){ logger.debug("slf4j-log4j info message"); } if(logger.isTraceEnabled()){ logger.debug("slf4j-log4j trace message"); } } }

1,LoggerFactory.getLogger(Slf4jTest.class); 得到Logger的对象。

1.1 方法链:getILoggerFactory->performInitialization->bind->findPossibleStaticLoggerBinderPathSet()

findPossibleStaticLoggerBinderPathSet:从类路径中寻找org/slf4j/impl/StaticLoggerBinder.class类,可能有多个,比如你有log4j 和 logback 实现包,都包含StaticLoggerBinder.class。

reportMultipleBindingAmbiguity(staticLoggerBinderPathSet); 打印绑定情况如下

SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [slf4j-log4j12-1.7.12.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [logback-classic-1.1.3.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]

说明:slf4j与其他实际的日志框架的集成jar包中,都会含有这样的一个org/slf4j/impl/StaticLoggerBinder.class类文件,并且提供一个ILoggerFactory的实现。

// the next line does the binding 随机选取绑定 StaticLoggerBinder

StaticLoggerBinder.getSingleton();

// 这里只有log4j ,所有返回Log4jLoggerFactory

return StaticLoggerBinder.getSingleton().getLoggerFactory();

1.2 iLoggerFactory.getLogger(name); 得到logger对象

->log4jLogger = LogManager.getLogger(name);

->logger = factory.makeNewLoggerInstance(name)->new Logger(name);

public class Log4jLoggerFactory implements ILoggerFactory { ConcurrentMap<String, Logger> loggerMap = new ConcurrentHashMap(); ... public Logger getLogger(String name) { org.apache.log4j.Logger log4jLogger; if (name.equalsIgnoreCase("ROOT")) { log4jLogger = LogManager.getRootLogger(); } else { //得到log4jLogger对象 //LogManager.java静态代码块会初始化Log4j,读取配置log4j.properties文件信息 log4jLogger = LogManager.getLogger(name); } //适配器模式转换 Logger newInstance = new Log4jLoggerAdapter(log4jLogger); //缓存Logger对象 Logger oldInstance = (Logger)this.loggerMap.putIfAbsent(name, newInstance); return (Logger)(oldInstance == null ? newInstance : oldInstance); } }

2 logger.debug("slf4j-log4j debug message"); 如何实现输出的呢?

查看调用链路

log4j漏洞怎么发现的(002-slf4jlog4j使用及源码分析)(7)

可以找到下面代码:

this.qw.write(this.layout.format(event));--> Writer out; out.write(string);

使用layout格式化传入的message, 用输出流写入。

this.qw.flush();

qw=QuietWriter(osw,handler)

OutputStreamWriter osw = new OutputStreamWriter(System.out);

到此logger.debug方法输出,主线源码已分析完。

更多参考:https://my.oschina.net/xianggao/blog/519199

,