动态调整日志级别
背景
在项目日常运维过程中,经常会出现某个线上问题需要通过一些更详细的debug级别的日志来辅助分析,但项目实际运行时,为了避免日志体量过大,通常仅会保留相对重要的异常日志、业务日志,日志级别会设置为info甚至warn或者error,如果想要通过debug日志来分析问题就必须调整日志级别,用于在下次执行代码逻辑时将更加详细的debug日志输出。
实现方案
用于调整日志级别的方式有很多种,例如:
- 使用配置中心时,可通过修改配置文件来调整日志级别;
- 未使用配置中心时,可通过自定义代码来调整日志级别;
- 集成
actuator,开放loggers端点,通过调用actuator的接口来调整日志级别(可以通过集成spring-boot-admin从页面进行调整操作); - ……
自定义代码调整日志级别
这里我们围绕常用的日志框架logback、log4j、log4j2,通过分析org.slf4j.Logger的加载过程,来获取如何通过自定义代码来动态调整日志级别。
一个简单的 demo:github 链接 | gitee 链接
本文下述均基于 SpringBoot 3.1.0、JDK 17`。
level 如何修改?
先上修改方式,然后在分析logback、log4j、log4j2分别是如何实现的:
本示例使用spring-boot-starter-web默认的spring-boot-starter-logging
import ch.qos.logback.classic.Level; |
本示例使用1.3.8.RELEASE版本的spring-boot-starter-log4j:
import org.apache.log4j.Level; |
本示例使用spring-boot-dependencies下管理的spring-boot-starter-log4j2
import org.apache.logging.log4j.Level; |
org.slf4j.Logger 如何加载?
众所周知,我们的logger通常是通过如下代码来定义的:
package com.example; |
上述代码表示通过org.slf4j.LoggerFactory#getLogger获取名为"com.example.ServiceImpl"的org.slf4j.Logger。
而org.slf4j.Logger中没有设置level的相关方法,因此我们不能简单的通过该logger来修改日志级别,那如何来修改日志级别呢?
我们接着分析org.slf4j.LoggerFactory#getLogger的实现逻辑,可以发现首先要通过调用getILoggerFactory()来获取org.slf4j.ILoggerFactory,然后再调用org.slf4j.ILoggerFactory实例的getLogger()方法来获取org.slf4j.Logger,如下图代码片段所示:

我们接下来就重点分析在三个常用日志框架中,org.slf4j.ILoggerFactory的实现是谁?org.slf4j.Logger的实现是谁?org.slf4j.ILoggerFactory的实现类中是如何实现getLogger()方法的?
org.slf4j.ILoggerFactory 的实现是谁?
我们先来分析getILoggerFactory()方法,可以发现首先要通过调用getProvider()来获取org.slf4j.spi.SLF4JServiceProvider,然后再调用org.slf4j.spi.SLF4JServiceProvider的getLoggerFactory()方法来获取org.slf4j.ILoggerFactory,如下图代码片段所示:

我们先来看getProvider()方法,在未初始化时会调用performInitialization()来执行初始化过程,在初始化成功后会返回PROVIDER(org.slf4j.spi.SLF4JServiceProvider),如下图代码片段所示:

我们再来看performInitialization()方法的实现逻辑,可以发现最终通过调用findServiceProviders()获取到所有的org.slf4j.spi.SLF4JServiceProvider,然后将第一个赋值给PROVIDER,并调用PROVIDER.initialize()执行SLF4JServiceProvider的初始化,如下图代码片段所示:


findServiceProviders()方法的具体实现逻辑这里就不展开描述了,底层是通过spi机制来获取org.slf4j.spi.SLF4JServiceProvider的实现,我们逐个分析各日志框架实现类中getLoggerFactory()返回的org.slf4j.ILoggerFactory的实现具体是什么。
org.slf4j.spi.SLF4JServiceProvider的实现为ch.qos.logback.classic.spi.LogbackServiceProvider。

我们先来看getLoggerFactory()方法,最终返回的为defaultLoggerContext变量,而defaultLoggerContext变量在执行initialize()时指向ch.qos.logback.classic.LoggerContext的实例,如下图代码片段所示:

我们再来看ch.qos.logback.classic.LoggerContext,发现其恰好实现了org.slf4j.ILoggerFactory,如下图代码片段所示:

由此,我们可以得出结论,使用logback时,org.slf4j.ILoggerFactory的实现为ch.qos.logback.classic.LoggerContext。
org.slf4j.spi.SLF4JServiceProvider的实现为org.slf4j.reload4j.Reload4jServiceProvider。

我们先来看getLoggerFactory()方法,最终返回的为loggerFactory变量,而loggerFactory变量在执行initialize()时指向org.slf4j.reload4j.Reload4jLoggerFactory的实例,如下图代码片段所示:

我们再来看org.slf4j.reload4j.Reload4jLoggerFactory,发现其恰好实现了org.slf4j.ILoggerFactory,如下图代码片段所示:

由此,我们可以得出结论,使用log4j时,org.slf4j.ILoggerFactory的实现为org.slf4j.reload4j.Reload4jLoggerFactory。
org.slf4j.spi.SLF4JServiceProvider的实现为org.apache.logging.slf4j.SLF4JServiceProvider。

我们先来看getLoggerFactory()方法,最终返回的为loggerFactory变量,而loggerFactory变量在执行initialize()时指向org.apache.logging.slf4j.Log4jLoggerFactory的实例,如下图代码片段所示:

我们再来看org.apache.logging.slf4j.Log4jLoggerFactory,发现其恰好实现了org.slf4j.ILoggerFactory,如下图代码片段所示:

由此,我们可以得出结论,使用log4j2时,org.slf4j.ILoggerFactory的实现为org.apache.logging.slf4j.Log4jLoggerFactory。
org.slf4j.Logger 的实现是谁?
我们先来看各个框架是如何实现org.slf4j.ILoggerFactory#getLogger,在这个过程中寻找org.slf4j.Logger的实现,以及分析是否可以通过org.slf4j.Logger的实现类来调整日志级别。
我们先来看ch.qos.logback.classic.LoggerContext#getLogger,可以发现是通过调用ch.qos.logback.classic.Logger#createChildByName来获取ch.qos.logback.classic.Logger,并缓存到loggerCache中,如下图代码片段所示:


我们再来看ch.qos.logback.classic.Logger,发现其恰好实现了org.slf4j.Logger,而且其有公共的setLevel()方法用于修改日志级别,如下图代码片段所示:


由此,我们可以得出结论,使用logback时,org.slf4j.Logger的实现为ch.qos.logback.classic.Logger。
我们先来看org.slf4j.reload4j.Reload4jLoggerFactory#getLogger,可以发现是通过调用org.apache.log4j.LogManager#getLogger来获取org.apache.log4j.Logger,并包装为org.slf4j.reload4j.Reload4jLoggerAdapter,如下图代码片段所示:

我们再来看org.slf4j.reload4j.Reload4jLoggerAdapter,发现其恰好实现了org.slf4j.Logger,而且其代理了org.apache.log4j.Logger,而org.apache.log4j.Logger的父类org.apache.log4j.Category中有公共的setLevel()方法用于修改日志级别,如下图代码片段所示:



由此,我们可以得出结论,使用log4j时,org.slf4j.Logger的实现为org.slf4j.reload4j.Reload4jLoggerAdapter。
我们先来看org.apache.logging.slf4j.Log4jLoggerFactory#getLogger,可以发现getLogger()的实现实际是在其父类org.apache.logging.log4j.spi.AbstractLoggerAdapter中,要先调用getContext()获取org.apache.logging.log4j.spi.LoggerContext,然后调用getLoggersInContext()来获取ConcurrentMap,然后再从ConcurrentMap中获取org.slf4j.Logger,若不存在,则调用newLogger()创建,如下图代码片段所示:

我们再来看getContext(),可知是通过调用org.apache.logging.log4j.LogManager#getContext来获取org.apache.logging.log4j.spi.LoggerContext,如下图代码片段所示:


我们再来看getLoggersInContext(),仅仅只是为了从registry中获取当前org.apache.logging.log4j.spi.LoggerContext对应的org.slf4j.Logger集,如下图代码片段所示:

我们再来看newLogger(),实际上是调用org.apache.logging.log4j.spi.LoggerContext#getLogger来获取org.apache.logging.log4j.spi.ExtendedLogger,并包装为org.apache.logging.slf4j.Log4jLogger,如下图代码片段所示:

这里org.apache.logging.log4j.spi.LoggerContext的实现为org.apache.logging.log4j.core.LoggerContext,而最终得到的org.apache.logging.log4j.spi.ExtendedLogger实现为org.apache.logging.log4j.core.Logger,如下图代码片段所示:(直接给结论,可自行 debug 验证)

最后我们再来看org.apache.logging.slf4j.Log4jLogger,发现其恰好实现了org.slf4j.Logger,而其代理了org.apache.logging.log4j.core.Logger,org.apache.logging.log4j.core.Logger中有公共的setLevel()方法用于修改日志级别,如下图代码片段所示:


由此,我们可以得出结论,使用log4j2时,org.slf4j.Logger的实现为org.apache.logging.slf4j.Log4jLogger。
日志级别如何动态调整?
经过上述的分析,可以发现ch.qos.logback.classic.Logger由其本身的createChildByName()方法来完成创建,由ch.qos.logback.classic.LoggerContext实例的私有变量loggerCache来缓存,所以要想调整日志级别,必须先获取LoggerContext实例,而LoggerContext实例则保存在LogbackServiceProvider中,并由getLoggerFactory()方法对外提供调用,我们又知道getLoggerFactory()是在org.slf4j.LoggerFactory#getILoggerFactory中被调用的,而且该方法为静态工具方法,由此我们可以验证前面的修改日志级别的方法是正确的。
经过上述的分析,可以发现org.apache.log4j.Logger可以通过调用org.apache.log4j.LogManager#getLogger来获取,由此我们可以验证前面的修改日志级别的方法是正确的。
经过上述的分析,可以发现org.apache.logging.log4j.core.Logger可以通过调用org.apache.logging.log4j.core.LoggerContext#getLogger来获取,而LoggerContext实例又可以通过org.apache.logging.log4j.core.LoggerContext#getContext(false)来获取,其底层也是基于org.apache.logging.log4j.LogManager#getContext(false)来获取,由此我们可以验证前面的修改日志级别的方法是正确的。