导读:笔者在实际开发中需要实现 Logging Level 动态调整。本文将讨论 Springboot 项目借助 Apollo 实现动态调整 Logging Level 的方式及实现原理,希望对各位有所帮助。
环境
- JDK 8
- apollo 1.9.0
服务端配置
在 Apollo 新建的应用都会创建一个默认 application.properties 的 Namespace,由于笔者打算维持原项目采用 yml 的形式进行配置管理,所以创建一个私有的 application.yml Namespace,并将项目中的配置信息贴入。这里我们主要关注 logging.level.{loggerName} 这个日志级别配置项。
启动项目测试尝试打印日志
public void printLogger() throws Exception{
logger.info("我是info级别日志");
logger.error("我是error级别日志");
logger.warn("我是warn级别日志");
logger.debug("我是debug级别日志");
}
由于日志级别设置为 ERROR,所以只会打印出 ERROR 级别的日志
客户端实现
项目中关于接入 Apollo 的基础配置可参考 实践:把Springboot项目配置迁移至 Apollo 配置管理中心 ,这里就不再赘述。
1、开启额外配置参数
根据官方文档,要把日志相关配置放在 Apoolo 中管理,使 Apollo 的加载顺序放到日志系统加载之前,需要在本地项目中的 application.properties 中加入下面的配置参数,并开启。
apollo.bootstrap.eagerLoad.enabled=true
2、客户端 LoggingLevel 刷新服务具体实现
Apollo 默认支持 @Value(${someKey:someDefaultValue}) 在运行时自动更新(可通过 apollo.autoUpdateInjectedSpringProperties=true 参数控制),但要实现 LoggerLevel 动态变化则需结合 Apollo 监听配置变化事件,并借助 Spring Boot 提供了抽象日志系统(org.springframework.boot.logging.LoggingSystem) 实现修改日志级别的目的。
具体实现如下:
import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Set;
/**
* 动态修改日志级别
*/
@Service
public class LoggingLevelRefresher {
private static final Logger log = LoggerFactory.getLogger(LoggingLevelRefresher.class);
private static final String PREFIX = "logging.level.";
private static final String ROOT = LoggingSystem.ROOT_LOGGER_NAME;
private static final String SPLIT = ".";
@Resource
private LoggingSystem loggingSystem;
/**
* 这里的 Config 默认是获取 namespace 为 application.properties,如果监听其他配置文件则需指定 namespace
*/
@ApolloConfig(value = "application.yml")
private Config config;
@PostConstruct
private void init() {
refreshLoggingLevels(config.getPropertyNames());
}
/**
* 监听也需指定具体监听的 namespace
*/
@ApolloConfigChangeListener(value = "application.yml" ,interestedKeyPrefixes = PREFIX)
private void onChange(ConfigChangeEvent changeEvent) {
refreshLoggingLevels(changeEvent.changedKeys());
}
private void refreshLoggingLevels(Set<String> changedKeys) {
for (String key : changedKeys) {
if (containsIgnoreCase(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);
}
}
}
private String parentStrLevel(String loggerName) {
String parentLoggerName = loggerName.contains(SPLIT) ? loggerName.substring(0, loggerName.lastIndexOf(SPLIT)) : ROOT;
return loggingSystem.getLoggerConfiguration(parentLoggerName).getEffectiveLevel().name();
}
/**
* 获取当前类的Logger对象有效日志级别对应的方法,进行日志输出。举例:
* 如果当前类的EffectiveLevel为WARN,则获取的Method为 `org.slf4j.Logger#warn(java.lang.String, java.lang.Object, java.lang.Object)`
* 目的是为了输出`changed {} log level to:{}`这一行日志
*/
private void log(String loggerName, String strLevel) {
try {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(log.getName());
Method method = log.getClass().getMethod(loggerConfiguration.getEffectiveLevel().name().toLowerCase(), String.class, Object.class, Object.class);
method.invoke(log, "changed {} log level to:{}", loggerName, strLevel);
} catch (Exception e) {
log.error("changed {} log level to:{} error", loggerName, strLevel, e);
}
}
private static boolean containsIgnoreCase(String str, String searchStr) {
if (str == null || searchStr == null) {
return false;
}
int len = searchStr.length();
int max = str.length() - len;
for (int i = 0; i <= max; i++) {
if (str.regionMatches(true, i, searchStr, 0, len)) {
return true;
}
}
return false;
}
}
3、代码解析
3.1 要注意第 36 行代码 @ApolloConfig(value = "application.yml") 这个注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface ApolloConfig {
String value() default "application";
}
其默认指定是 application.properties,由于笔者并没有将配置配在该 namespace 中,而是配置了新建的 application,yml。一开始没注意该注解有属性,导致一直获取 config 为 Null。
3.2 第 47 行代码 @ApolloConfigChangeListener(value = "application.yml" ,interestedKeyPrefixes = PREFIX) 监听指定配置文件变化的注解。其具有三个属性,也需指定具体要监听的配置文件,否则默认监听 application.properties
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApolloConfigChangeListener {
String[] value() default {"application"};
String[] interestedKeys() default {};
String[] interestedKeyPrefixes() default {};
}
3.3 最终通过 loggingSystem.setLogLevel(loggerName, level); 改变了日志级别。
Spring Boot 在构建 Spring 容器的生命过程中,初始化了日志系统 LoggingSystem 抽象类并绑定了指定的日志组件(如:logback、log4j2)。通过LoggingSystem 屏蔽了不同日志组件之间的差异,又提供了 setLogLevel() 让我们能够方便地修改日志等级。
4、查看效果
将 Apollo 的 logging.level 设置为 info ,启动本地 Springboot 项目打印日志,接着修改 logging.level 为 error 并发布生效,再次打印日志。结果如下图所示,实现了动态改变日志打印级别。
总结
本文实现日志级别动态变更主要借助 Apollo 配置中心使应用能够准实时监听地获取配置中 Logger 日志级别值变化。同时借助 LoggingSystem 日志系统提供的 setLogLevel() 接口修改日志级别。
最后
感谢您的阅读,如果喜欢本文欢迎关注和转发,转载需注明出处,本头条号将持续分享IT技术知识。对于文章内容有其他想法或意见建议等,欢迎提出共同讨论共同进步。
参考文档
https://github.com/apolloconfig/apollo-use-cases/tree/master/spring-boot-logger
https://blog.csdn.net/qq271859852/article/details/103230470