概述
日志模块是每个项目中必须的,用来记录程序运行中的相关信息。一般在开发环境下使用DEBUG级别的日志输出,为了方便查看问题,而在线上一般都使用INFO级别的日志,主要记录业务操作的日志。那么问题来了,当线上环境出现问题希望输出DEBUG日志信息辅助排查的时候怎么办呢?修改配置文件,重新打包然后上传重启线上环境,但是这么做不优雅 而且可能会破坏现场。
本文介绍一种实现方案:通过Apollo配置中心来实现 动态调整线上日志级别。
日志级别
不同的日志框架支持不同的日志级别,其中比较常见的就是Log4j和Logback。
在Log4j中支持8种日志级别,优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。
Logback中支持7种日志级别,优先级从高到低分别是:OFF、ERROR、WARN、INFO、DEBUG、TRACE、ALL。
可以看到常见的ERROR、WARN、INFO、DEBUG,这两者都是支持的。
所谓设置日志的输出级别表示的是输出的日志的最低级别,也就是说,如果我们把级别设置成INFO,那么包括INFO在内以及比INFO优先级高的级别的日志都可以输出。
Spring Boot对日志的支持
Spring Boot 对log做了统一封装,代码在 org.springframework.boot.logging 包中,结构如下:
其中 org.springframework.boot.logging.LoggingSystem 是SpringBoot对日志系统的抽象,是一个顶层的抽象类,有很多具体的实现:
通过上图,我们可以发现目前SpringBoot目前支持3种类型的日志,分别是
- JDK内置的Log(JavaLoggingSystem)
- Log4j2(Log4J2LoggingSystem)
- Logback(LogbackLoggingSystem)。
static {
Map<String, String> systems = new LinkedHashMap<>();
systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
SYSTEMS = Collections.unmodifiableMap(systems);
}
LoggingSystem是个抽象类,内部有这几个方法:
- beforeInitialize方法:日志系统初始化之前需要处理的事情。抽象方法,不同的日志架构进行不同的处理
- initialize方法:初始化日志系统。默认不进行任何处理,需子类进行初始化工作
- cleanUp方法:日志系统的清除工作。默认不进行任何处理,需子类进行清除工作
- getShutdownHandler方法:返回一个Runnable用于当jvm退出的时候处理日志系统关闭后需要进行的操作,默认返回null,也就是什么都不做
- getSupportedLogLevels: 返回日志系统实际支持的一组 LogLevel。
- setLogLevel方法:抽象方法,用于设置对应logger的级别
- get方法:检测并返回正在使用的日志系统。支持 Logback 和 Java 日志记录。
代码
我们可以将日志级别配置保存在Apollo配置中心中, 当日志级别发生变更时,我们需要通过监听该配置的变更,设置应用中的 Logger 的日志级别,从而后续的日志打印可以根据新的日志级别
@Slf4j
@Component
public class LoggingSystemAdjustListener {
/**
* 日志配置项的前缀
*/
private static final String LOGGER_PREFIX = "logging.level.";
@Resource
private LoggingSystem loggingSystem;
// By default only read config in "application"
@ApolloConfigChangeListener
public void onChange(ConfigChangeEvent changeEvent) throws Exception {
// <Y> 遍历配置集的每个配置项,判断是否是 logging.level 配置项
for (String key : changeEvent.changedKeys()) {
// 如果是 logging.level 配置项,则设置其对应的日志级别
if (key.startsWith(LOGGER_PREFIX)) {
String loggerName = key.replace(LOGGER_PREFIX, "");
//
LoggerConfiguration cfg = loggingSystem.getLoggerConfiguration(loggerName);
if (cfg == null) {
if (log.isErrorEnabled()) {
log.error("no loggerConfiguration with loggerName:{}", loggerName);
}
continue;
}
// 获得日志级别
ConfigChange change = changeEvent.getChange(key);
// the newLevel could be null if the config is deleted from apollo
// in this case we update it same as "root" level
String newLevel = change.getNewValue();
LogLevel level = null;
// config is deleted or kept as empty string
if (newLevel == null || newLevel.isEmpty()) {
level = getFallbackLogLevel(ROOT_LOGGER_NAME);
} else {
try {
level = LogLevel.valueOf(newLevel.toUpperCase());
} catch (IllegalArgumentException e) {
// do nothing
}
}
if (level == null) {
if (log.isErrorEnabled()) {
log.error("logger:[{}]current LogLevel is invalid:{}", loggerName, newLevel);
}
continue;
}
if (!isSupportLevel(level)) {
if (log.isErrorEnabled()) {
log.error("LoggingSystem:[] not support current LogLevel:{}",
loggingSystem.getClass().getName(), newLevel);
}
continue;
}
if (log.isInfoEnabled()) {
log.info("logger:[{}] current effective level:{}, to be changed to level:{}", loggerName,
cfg.getEffectiveLevel(), newLevel);
}
// 基于springboot的日志抽象类,设置日志级别到 LoggingSystem 中
loggingSystem.setLogLevel(loggerName, level);
}
}
}
private boolean isSupportLevel(LogLevel level) {
for (LogLevel ll : loggingSystem.getSupportedLogLevels()) {
if (ll == level) {
return true;
}
}
return false;
}
public LogLevel getFallbackLogLevel(String loggerName) {
LoggerConfiguration cfg = loggingSystem.getLoggerConfiguration(loggerName);
if (cfg == null) {
if (log.isErrorEnabled()) {
log.error("no loggerConfiguration with loggerName:{}", loggerName);
}
// use WARN as unexpected case
return LogLevel.WARN;
}
return cfg.getEffectiveLevel();
}
}
基于spring的日志支持,我们还可以在logback中通过logger标签对某一个包甚至类单独配置日志级别
<logger name="com.ethan.demo.log.controller" level="INFO" additivity="true">
<appender-ref ref="${CONSOLE_APPENDER}"/>
</logger>