原创

Apollo配置中心动态刷新日志级别:原理解析

Apollo配置中心动态刷新日志级别

源码分析

源码基于Spring Boot 2.4.4

Spring Boot应用启动

在Spring Boot应用启动,运行到Spring容器的生命周期节点(扩展点)时,Spring会发出一些通知事件,例如ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent、ApplicationPreparedEvent等等,让我们可以有机会监听这些事件,并且进行相应的处理。

Spring 内部也定义了一系列监听器,用于监听生命周期事件,来进行扩展(思想:微内核 + 插件)。

如下所示,Spring Boot内部定义了 org.springframework.boot.context.logging.LoggingApplicationListener,并且监听了 ApplicationEvent事件,里面有个对 ApplicationStartingEvent 事件的判断处理。在 ApplicationStartingEvent 事件中,构造了日志系统 LoggingSystem,并且执行初始化之前的回调 loggingSystem.beforeInitialize(),为初始化做准备。

public class LoggingApplicationListener implements GenericApplicationListener {

    private static final ConfigurationPropertyName LOGGING_LEVEL = ConfigurationPropertyName.of("logging.level");

    private static final ConfigurationPropertyName LOGGING_GROUP = ConfigurationPropertyName.of("logging.group");

    /**
     * The name of the Spring property that contains a reference to the logging
     * configuration to load.
     */
    public static final String CONFIG_PROPERTY = "logging.config";

    ... ...

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationStartingEvent) {
            onApplicationStartingEvent((ApplicationStartingEvent) event);
        }
        else if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
        }
        else if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent((ApplicationPreparedEvent) event);
        }
        else if (event instanceof ContextClosedEvent
                && ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
            onContextClosedEvent();
        }
        else if (event instanceof ApplicationFailedEvent) {
            onApplicationFailedEvent();
        }
    }

    ... ...

    private void onApplicationStartingEvent(ApplicationStartingEvent event) {
        this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
        this.loggingSystem.beforeInitialize();
    }
    ... ...

接着看org.springframework.boot.logging.LoggingSystem#get(java.lang.ClassLoader)

LoggingSystem 抽象类

public abstract class LoggingSystem {
    /**
     * A System property that can be used to indicate the {@link LoggingSystem} to use.

     *  如: org.springframework.boot.logging.LoggingSystem
     */
    public static final String SYSTEM_PROPERTY = LoggingSystem.class.getName();

    /**
     * The value of the {@link #SYSTEM_PROPERTY} that can be used to indicate that no
     * {@link LoggingSystem} should be used.
     */
    public static final String NONE = "none";

    // get   LoggingSystemFactory, 对应实现类  DelegatingLoggingSystemFactory
    private static final LoggingSystemFactory SYSTEM_FACTORY = LoggingSystemFactory.fromSpringFactories();

    ... ...

    /**
     * Detect and return the logging system in use. Supports Logback and Java Logging.
     * @param classLoader the classloader
     * @return the logging system
     */
    public static LoggingSystem get(ClassLoader classLoader) {
        String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
        if (StringUtils.hasLength(loggingSystemClassName)) {
            if (NONE.equals(loggingSystemClassName)) {
                return new NoOpLoggingSystem();
            }
            return get(classLoader, loggingSystemClassName);
        }

        // action is : DelegatingLoggingSystemFactory.getLoggingSystem(ClassLoader)
        LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
        Assert.state(loggingSystem != null, "No suitable logging system located");
        return loggingSystem;
    }

    ... ...
}

DelegatingLoggingSystemFactory 类: LoggingSystem 代理处理工厂

class DelegatingLoggingSystemFactory implements LoggingSystemFactory {

    private final Function<ClassLoader, List<LoggingSystemFactory>> delegates;

    /**
     * Create a new {@link DelegatingLoggingSystemFactory} instance.
     * @param delegates a function that provides the delegates
     */
    DelegatingLoggingSystemFactory(Function<ClassLoader, List<LoggingSystemFactory>> delegates) {
        this.delegates = delegates;
    }

    @Override
    public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
        // 排序为 LogbackLoggingSystem、Log4J2LoggingSystem、LogbackLoggingSystem
        List<LoggingSystemFactory> delegates = (this.delegates != null) ? this.delegates.apply(classLoader) : null;
        if (delegates != null) {
            for (LoggingSystemFactory delegate : delegates) {
                LoggingSystem loggingSystem = delegate.getLoggingSystem(classLoader);
                if (loggingSystem != null) {
                    // 得到实例立即返回。 优先级低的直接无视
                    return loggingSystem;
                }
            }
        }
        return null;
    }

}

通过上述源码,我们知道有两种方式指定底层日志组件:

  • 通过环境变量指定。例如下述方式指定了Logback做为底层日志组件

    -Dorg.springframework.boot.logging.LoggingSystem=org.springframework.boot.logging.logback.LogbackLoggingSystem

  • 按Spring Boot预定义的日志系统顺序查找,排在前面的日志组件优先级高。
    源码 LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader)解析:
    LoggingSystemFactory 为一个接口。 存在4个实现类:
    file

debug 追踪查看 SYSTEM_FACTORY.getLoggingSystem(classLoader) 内部的 delegates 私有常量, 与上述一致:
file

可以看到,LoggingSystem支持的日志组件,按顺序有如下三种

  • Logback
  • Log4j2
  • JavaLog(java.util.logging)

PS:低版本 Spring Boot 2.1.10.RELEASE 版本中额外多了一种:Log4j
file

一般情况下我们不会手动指定环境变量,而是采用一种约定优于配置的思想,交由Spring Boot判断:只要存在Logback相关类,就认为Logback应该生效作为底层的日志组件,其它的依此类推。

源码从侧面也透漏着一个信息:Spring Boot偏爱Logback。

继续

之后,Spring会发出 ApplicationEnvironmentPreparedEvent 事件,并且仍由 LoggingApplicationListener 进行监听,在监听时进行了日志组件的初始化,如此,一个日志系统(LoggingSystem)便构造完毕

public class LoggingApplicationListener implements GenericApplicationListener {
    ... ...

    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        if (this.loggingSystem == null) {
            this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
        }

        // 即为 ConfigurableEnvironment 环境对象。
        initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
    }

     ... ...

     /**
     * Initialize the logging system according to preferences expressed through the
     * {@link Environment} and the classpath.
     * @param environment the environment
     * @param classLoader the classloader
     */
    protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
        getLoggingSystemProperties(environment).apply();
        this.logFile = LogFile.get(environment);
        if (this.logFile != null) {
            this.logFile.applyToSystemProperties();
        }
        this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
        initializeEarlyLoggingLevel(environment);
        initializeSystem(environment, this.loggingSystem, this.logFile);
        initializeFinalLoggingLevels(environment, this.loggingSystem);
        registerShutdownHookIfNecessary(environment, this.loggingSystem);
    }
}

ConfigurableEnvironment 环境对象:event.getEnvironment()
file

新增或修改配置时

新增或修改配置时(比如添加配置项或修改配置项 logging.level.{loggerName} = DEBUG ), 会触发Apollo拉取最新的配置信息,并且将变更内容进行回调。

在回调事件中,通过获取配置的日志级别,调用 LoggingSystem#setLogLevel 方法调整对应logger的日志级别;

自定义 DynamicsLoggingLevelRefresher 类

public class DynamicsLoggingLevelRefresher {
    @ApolloConfigChangeListener(interestedKeyPrefixes = PREFIX)
    private void onChange(ConfigChangeEvent changeEvent) {
        refreshLoggingLevels(changeEvent.changedKeys());
    }

    private void refreshLoggingLevels(Set<String> changedKeys) {
        for (String key : changedKeys) {
            // key may be : logging.level.com.example.web
            if (StringUtils.startsWithIgnoreCase(key, PREFIX)) {
                String loggerName = PREFIX.equalsIgnoreCase(key) ? ROOT : key.substring(PREFIX.length());
                String strLevel = config.getProperty(key, parentStrLevel(loggerName));
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());

                //重置日志级别,马上生效
                loggingSystem.setLogLevel(loggerName, level);

                log(loggerName, strLevel);
            }
        }
    }
}

删除该配置项,同样会触发应用去Apollo拉取最新的配置信息,changedKeys 包含删掉的配置项,此时调用 Config#getProperty 必然获取不到配置中心的配置项信息(因为已经删除),因此getProperty第二个参数(parentStrLevel(loggerName))就是用于指定当获取的配置项值为null时的默认值。

此处,我们获取了父Logger的Level作为默认值,便达到了恢复的目的。

额外说明

此处需要注意的一点是,源码实现中如果是 Logback 日志组件, 可以完全直接使用。但如果不是 Logback 日志组件, 则需要局部微调: loggingSystem.getLoggerConfiguration(parentLoggerName).getEffectiveLevel().name(),缘由是在获取父Logger的EffectiveLevel实现方式上取了巧,如果使用的是Log4j2,会出现空指针异常---->究其原因,日志组件底层实现机制不同,行为也就不一样。

总结

Spring Boot 在构建Spring容器的生命过程中,初始化了日志系统 LoggingSystem,并和某种日志组件如Logback进行了绑定。

如此,通过LoggingSystem暴露出来的setLogLevel接口,屏蔽了不同日志组件之间的差异,忽略底层日志组件存在的同时,又能在需要时刻调用接口修改日志级别(抽象的魅力),借助配置中心(如Apollo、diamond、ZK、Nacos、Spring Cloud Config + Spring Cloud Bus等)的变更即推送能力,应用能够准实时获取所配置的Logger日志级别,并调用LoggingSystem#setLogLevel进行日志级别的设置。

参考文章

参考文章:
https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.logging.logback

~ end

正文到此结束
广告是为了更好的提供数据服务
本文目录