动态刷新日志级别

概述

日志模块是每个项目中必须的,用来记录程序运行中的相关信息。一般在开发环境下使用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种类型的日志,分别是

  1. JDK内置的Log(JavaLoggingSystem)
  2. Log4j2(Log4J2LoggingSystem)
  3. 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是个抽象类,内部有这几个方法:

在这里插入图片描述

  1. beforeInitialize方法:日志系统初始化之前需要处理的事情。抽象方法,不同的日志架构进行不同的处理
  2. initialize方法:初始化日志系统。默认不进行任何处理,需子类进行初始化工作
  3. cleanUp方法:日志系统的清除工作。默认不进行任何处理,需子类进行清除工作
  4. getShutdownHandler方法:返回一个Runnable用于当jvm退出的时候处理日志系统关闭后需要进行的操作,默认返回null,也就是什么都不做
  5. getSupportedLogLevels: 返回日志系统实际支持的一组 LogLevel。
  6. setLogLevel方法:抽象方法,用于设置对应logger的级别
  7. 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>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个封装了spdlog的复杂日志系统的示例代码: ```python import os import time from enum import IntEnum from concurrent.futures import ThreadPoolExecutor from typing import Optional import spdlog class LogLevel(IntEnum): TRACE = 0 DEBUG = 1 INFO = 2 WARN = 3 ERROR = 4 CRITICAL = 5 class Color(IntEnum): DEFAULT = 0 BLACK = 30 RED = 31 GREEN = 32 YELLOW = 33 BLUE = 34 MAGENTA = 35 CYAN = 36 LIGHT_GRAY = 37 DARK_GRAY = 90 LIGHT_RED = 91 LIGHT_GREEN = 92 LIGHT_YELLOW = 93 LIGHT_BLUE = 94 LIGHT_MAGENTA = 95 LIGHT_CYAN = 96 WHITE = 97 class Log: def __init__(self, logger_name: str, log_file: Optional[str] = None, console: bool = True, level: LogLevel = LogLevel.DEBUG, rotation_size: int = 1024 * 1024 * 10, max_files: int = 10): self.logger_name = logger_name self.console = console self.level = level self.rotation_size = rotation_size self.max_files = max_files self.log_file = log_file or f'{self.logger_name}.log' self.executor = ThreadPoolExecutor(max_workers=1) self.logger = None self.init_logger() def init_logger(self): self.logger = spdlog.create(self.logger_name, sinks=[], level=self.level) if self.console: console_sink = spdlog.stdout_color_sink_mt() colored_formatter = spdlog.pattern_formatter("%^[%T] [%n] [%l] [%@] %v%$") console_sink.set_formatter(colored_formatter) self.logger.sinks().push_back(console_sink) file_sink = spdlog.rotating_file_sink_mt(self.log_file, self.rotation_size, self.max_files) file_formatter = spdlog.pattern_formatter("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] [%@] %v") file_sink.set_formatter(file_formatter) self.logger.sinks().push_back(file_sink) def log(self, level: LogLevel, msg: str, file: Optional[str] = None, line: Optional[int] = None): if level < self.level: return if file and line: msg = f'{msg} ({os.path.basename(file)}:{line})' if level == LogLevel.TRACE: self.logger.trace(msg) elif level == LogLevel.DEBUG: self.logger.debug(msg) elif level == LogLevel.INFO: self.logger.info(msg) elif level == LogLevel.WARN: self.logger.warn(msg) elif level == LogLevel.ERROR: self.logger.error(msg) elif level == LogLevel.CRITICAL: self.logger.critical(msg) def trace(self, msg: str, file: Optional[str] = None, line: Optional[int] = None): self.log(LogLevel.TRACE, msg, file, line) def debug(self, msg: str, file: Optional[str] = None, line: Optional[int] = None): self.log(LogLevel.DEBUG, msg, file, line) def info(self, msg: str, file: Optional[str] = None, line: Optional[int] = None): self.log(LogLevel.INFO, msg, file, line) def warn(self, msg: str, file: Optional[str] = None, line: Optional[int] = None): self.log(LogLevel.WARN, msg, file, line) def error(self, msg: str, file: Optional[str] = None, line: Optional[int] = None): self.log(LogLevel.ERROR, msg, file, line) def critical(self, msg: str, file: Optional[str] = None, line: Optional[int] = None): self.log(LogLevel.CRITICAL, msg, file, line) def flush(self): self.logger.flush() def async_flush(self): self.executor.submit(self.flush) def start_auto_flush(self, interval: int = 2): while True: time.sleep(interval) self.async_flush() ``` 使用示例: ```python log = Log('test', log_file='test.log', console=True, level=LogLevel.DEBUG, rotation_size=1024 * 1024 * 10, max_files=10) log.trace('Trace message') log.debug('Debug message') log.info('Info message') log.warn('Warn message') log.error('Error message') log.critical('Critical message') # 带文件名和行号 log.info('Info message', __file__, 20) # 刷新日志 log.flush() # 异步刷新日志 log.async_flush() # 开启自动刷新日志 log.start_auto_flush() ``` 说明: 1. 日志系统使用了`spdlog`库,支持打印到控制台和日志文件,支持彩色输出。 2. 可以自由选择是否打印到控制台,设置日志级别,设置日志文件大小和数量,实现日志文件的轮换。 3. 日志输出可详细到文件名和行号,并支持异步刷新和自动刷新日志文件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值