SpringBoot结合Apollo配置中心实现日志级别动态配置

背景

目前常用的实现动态配置日志级别的应该非 SpringBootspring-boot-starter-actuator 莫属了。

不过通过 spring-boot-starter-actuator 配置的日志级别,服务一旦重启就会恢复原状。且只能通过访问指定的接口来修改单个实例的日志级别(SpringBootAdmin也是一样,只能修改单个实例的)。如果是想修改某个服务所有实例的日志级别,只能修改配置文件,然后重启服务,可以说局限性稍微大点儿。

由于重启服务太费劲儿,所以想到了利用 Apollo 配置中心来动态修改日志级别。

实现

大体思路是通过 Apollo 的监听机制,结合Spring的事件监听,来刷新日志级别。

具体代码如下:
LogLevelRefreshEvent :自定义Spring事件。

import lombok.ToString;
import org.springframework.context.ApplicationEvent;

/**
 * @author lifengdi
 * @date 2021/12/24 11:29
 */
@ToString
public class LogLevelRefreshEvent extends ApplicationEvent {

    /**
     * key
     */
    private final String key;

    /**
     * 旧值
     */
    private final Object oldValue;

    /**
     * 新值
     */
    private final Object newValue;

    public LogLevelRefreshEvent(String key, Object oldValue, Object newValue) {
        super(key);
        this.key = key;
        this.oldValue = oldValue;
        this.newValue = newValue;
    }

    public String getKey() {
        return key;
    }

    public Object getOldValue() {
        return oldValue;
    }

    public Object getNewValue() {
        return newValue;
    }
}

LogConfig :日志级别配置文件,用于获取 Apollo 配置以及转换日志级别。

import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
import lombok.Getter;
import org.springframework.boot.logging.LogLevel;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @author lifengdi
 * @date 2021/12/24 14:29
 */
@Configuration
@Getter
public class LogConfig {

    /**
     * 日志级别配置key
     */
    public static final String LOG_LEVEL_CONFIG_KEY = "logLevel.list";

    /**
     * 日志级别配置,格式:
     * <code>
     * {
     *     "com.cowell.conveyor": "warn",
     *     "com.cowell.tools": "warn"
     * }
     * </code>
     */
    @ApolloJsonValue("${logLevel.list:{}}")
    private Map<String, String> logLevelConfig;

    /**
     * 日志级别映射
     */
    private final Map<String, LogLevel> levelMap = new HashMap<String, LogLevel>() {
        {
            put("trace", LogLevel.TRACE);
            put("debug", LogLevel.DEBUG);
            put("info", LogLevel.INFO);
            put("warn", LogLevel.WARN);
            put("error", LogLevel.ERROR);
            put("fatal", LogLevel.FATAL);
            put("off", LogLevel.OFF);
        }
    };

    public Map<String, LogLevel> getLevelMap() {
        return levelMap;
    }

    /**
     * 根据名称获取日志级别,默认返回debug级别。
     * @param logLevel 级别名称
     * @return 日志级别 {@link LogLevel}
     */
    public LogLevel getFromLevelMap(String logLevel) {
        return levelMap.getOrDefault(logLevel, LogLevel.DEBUG);
    }
}

LogLevelUtils :工具类

import com.lifengdi.config.LogConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggingSystem;

import java.util.List;
import java.util.Map;

/**
 * @author lifengdi
 * @date 2021/12/27 17:44
 */
public class LogLevelUtils {

    private static final Logger logger = LoggerFactory.getLogger(LogLevelUtils.class);

    /**
     * 动态刷新日志级别
     * @param loggingSystem LoggingSystem
     * @param logConfig LogConfig
     */
    public static void refreshLogLevel(LoggingSystem loggingSystem, LogConfig logConfig) {
        Map<String, String> logLevelConfig = logConfig.getLogLevelConfig();
        List<LoggerConfiguration> loggerConfigurations = loggingSystem.getLoggerConfigurations();
        if (!logLevelConfig.isEmpty()) {
            logLevelConfig.forEach((loggerName, value) -> {
                String logLevel = value.toLowerCase();
                for (LoggerConfiguration loggerConfiguration : loggerConfigurations) {
                    if (loggerConfiguration.getName().startsWith(loggerName)) {
                        loggingSystem.setLogLevel(loggerName, logConfig.getFromLevelMap(logLevel));
                        logger.debug("LogLevelUtils|RefreshLogLevel|SUCCESS|loggerName:{},logLevel:{}", loggerName, logLevel);
                    }
                }
            });
        }
    }
}

日志级别刷新Listener:

import com.lifengdi.config.LogConfig;
import com.lifengdi.util.LogLevelUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * 日志级别刷新Listener
 *
 * @author lifengdi
 * @date 2021/12/24 11:34
 */
@Component
@Slf4j
public class LogLevelRefreshListener implements ApplicationListener<LogLevelRefreshEvent> {

    @Autowired
    private LoggingSystem loggingSystem;

    @Autowired
    private LogConfig logConfig;

    @Override
    public void onApplicationEvent(LogLevelRefreshEvent logLevelRefreshEvent) {
        if (LogConfig.LOG_LEVEL_CONFIG_KEY.equals(logLevelRefreshEvent.getKey())) {
            log.info("LogLevelRefreshListener|onApplicationEvent|refreshLogLevel|configRefreshEvent:{}", logLevelRefreshEvent);
            LogLevelUtils.refreshLogLevel(loggingSystem, logConfig);
            log.info("LogLevelRefreshListener|onApplicationEvent|refreshLogLevel|SUCCESS|configRefreshEvent:{}", logLevelRefreshEvent);
        }
    }

}

服务启动时日志级别刷新Listener:

import com.lifengdi.config.LogConfig;
import com.lifengdi.util.LogLevelUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * 服务启动时日志级别刷新Listener
 *
 * @author lifengdi
 * @date 2021/12/24 13:59
 */
@Component
public class LogLevelApplicationReadyEventListener implements ApplicationListener<ApplicationReadyEvent> {

    @Autowired
    private LoggingSystem loggingSystem;

    @Autowired
    private LogConfig logConfig;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {

        LogLevelUtils.refreshLogLevel(loggingSystem, logConfig);

    }
}

增加事件发布:

@Component
public class ApolloAutoRefreshBean {

    private final Logger logger = LoggerFactory.getLogger(ApolloAutoRefreshBean.class);

    @Autowired
    private ApplicationContext applicationContext;

    @ApolloConfigChangeListener
    public void onChange(ConfigChangeEvent changeEvent) {
        logger.info("ApolloAutoRefreshBean|onChange|apollo触发变更|param:namespace={}", changeEvent.getNamespace());
        for (String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
            logger.info("ApolloAutoRefreshBean|onChange|apollo数据key-value发生变更|key: {}, oldValue: {}, newValue: {}, changeType: {}",
                    change.getPropertyName(), change.getOldValue(), change.getNewValue(),
                    change.getChangeType());
            applicationContext.publishEvent(new LogLevelRefreshEvent(key, change.getOldValue(), change.getNewValue()));
        }
        applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
    }
}

配置中心配置的参数格式如下:

{
    "com.lifengdi.util": "info",
    "com.lifengdi.tools": "warn"
}

至此,动态配置日志级别算是搞定了。


原文:结合Apollo配置中心实现日志级别动态配置 - 李锋镝的博客 - LiFengdi.Com