13

求你了,别再随便打日志了,教你动态修改日志级别!

 3 years ago
source link: https://www.hollischuang.com/archives/5977
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
求你了,别再随便打日志了,教你动态修改日志级别!-HollisChuang's Blog

之前写过一篇文章《明明有自动清理,日志还是把我的服务干爆了!》,介绍过一次大促故障,是因为日志量激增,导致服务器差点挂掉。

在那次问题发生之后,我开发了一个简单的日志降级的小工具,通过配置的方式,动态推送日志级别,动态修改线上的日志输出级别。并且把这份配置的修改配置到我们的预案平台上,大促期间进行定时或者紧急预案处理。

那么,这篇文章就来简单介绍下思路以及代码实现。

在开始正文前简单介绍下日志级别,不同的日志框架支持不同的日志级别,其中比较常见的就是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优先级高的级别的日志都可以输出。

无论是Log4j还是Logback,都是通过日志的配置文件来控制日志输出级别的。这里就不详述了。

上面我们提到了Log4j和Logback,这两种都是比较常用的日志框架。

但是很多时候,我们在代码中打印日志并不是直接使用这种日志框架来进行的,而是依赖了一个日志门面来进行的,如slf4j、commons-logging等。

一般最最常用的方法就是通过slf4j提供的LoggerFactory的getLogger来获取Logger,然后进行日志打印。

private static final Logger LOGGER = LoggerFactory.getLogger(LoggerService.class);

public void test(){
    LOGGER.info("hollis log test");
}

当我们使用LoggerFactory.getLogger方法创建一个Logger对象的时候,会给他传入一个loggerName,通过这个loggerName来唯一识别一个Logger,如上面的方式就是使用LoggerService这个类的全路径名作为其loggerName。

loggerName是每一个Logger的配置信息一部分,除此之外还有日志输出级别等信息。

关于为什么不直接使用log4j和logback打印日志,我在《为什么阿里巴巴禁止工程师直接使用日志系统(Log4j、Logback)中的 API》中分析过。

Arthas改变日志级别

在开始介绍代码实现之前,先介绍一个工具,也可以帮助我们的动态修改日志级别。

那就是阿里开源的神器——Arthas (https://arthas.aliyun.com/doc/ )。

Arthas提供了一个logger命令,这个命令可以查看和更新logger信息,包括日志级别。

查看指定名字的logger信息

[arthas@2062]$ logger -n org.springframework.web
 name                                   org.springframework.web
 class                                  ch.qos.logback.classic.Logger
 classLoader                            sun.misc.Launcher$AppClassLoader@2a139a55
 classLoaderHash                        2a139a55
 level                                  null
 effectiveLevel                         INFO
 additivity                             true
 codeSource                             file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar

更新logger level

[arthas@2062]$ logger --name ROOT --level debug
update logger level success.

简单吧,使用一个命令就可以修改机日志级别了。

但是Arthas目前对于集群的支持并不是特别的友好,虽然他支持了通过Arthas Tunnel Server/Client 来远程管理/连接多个Agent,但是使用起来还不是很方便,并且对于命令的使用要求比较高。

还有就是我们系统通过一个工具,方便我们在大促期间通过预案方式动态调整日志级别,这方面使用arthas就不是很方便了。

我写的这个工具功能很简单,就是提供动态修改日志级别的入口,方便用户动态修改级别。

并且为了方便使用,我将他封装在一个Spring Boot Starter里面了,还有就是将他直接对接到公司内部的配置中心中,可以方便的通过配置中心一键修改日志级别。

首先看下其中最核心的功能,那就是动态修改日志级别的部分,代码如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggingSystem;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static org.springframework.boot.logging.LoggingSystem.ROOT_LOGGER_NAME;

/**
 * 日志级别设置服务类
 *
 * @author Hollis
 */
public class LoggerLevelSettingService {

    @Autowired
    private LoggingSystem loggingSystem;

    private static final Logger LOGGER = LoggerFactory.getLogger(LoggerLevelSettingService.class);

    public void setRootLoggerLevel(String level) {

        LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(ROOT_LOGGER_NAME);

        if (loggerConfiguration == null) {
            if (LOGGER.isErrorEnabled()) {
                LOGGER.error("no loggerConfiguration with loggerName " + level);
            }
            return;
        }

        if (!supportLevels().contains(level)) {
            if (LOGGER.isErrorEnabled()) {
                LOGGER.error("current Level is not support : " + level);
            }
            return;
        }

        if (!loggerConfiguration.getEffectiveLevel().equals(LogLevel.valueOf(level))) {
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("setRootLoggerLevel success,old level is  '" + loggerConfiguration.getEffectiveLevel()
                    + "' , new level is '" + level + "'");
            }
            loggingSystem.setLogLevel(ROOT_LOGGER_NAME, LogLevel.valueOf(level));
        }
    }

    private List<String> supportLevels() {
        return loggingSystem.getSupportedLogLevels().stream().map(Enum::name).collect(Collectors.toList());
    }
}

以上代码,就是根据用户传入的level的级别,将应用的ROOT日志输出级别修改掉。

这里面用到了一个关键的服务:org.springframework.boot.logging.LoggingSystem

这个服务是SpringBoot对日志系统的抽象,是一个顶层的抽象类。他有很多具体的实现:

16090525095099.jpg

通过上图,我们可以发现目前SpringBoot目前支持4种类型的日志,分别是JDK内置的Log(JavaLoggingSystem)以及Log4j(Log4JLoggingSystem)、Log4j2(Log4J2LoggingSystem)以及Logback(LogbackLoggingSystem)。

LoggingSystem是个抽象类,内部有这几个方法:

  • beforeInitialize方法:日志系统初始化之前需要处理的事情。抽象方法,不同的日志架构进行不同的处理
  • initialize方法:初始化日志系统。默认不进行任何处理,需子类进行初始化工作
  • cleanUp方法:日志系统的清除工作。默认不进行任何处理,需子类进行清除工作
  • getShutdownHandler方法:返回一个Runnable用于当jvm退出的时候处理日志系统关闭后需要进行的操作,默认返回null,也就是什么都不做
  • setLogLevel方法:抽象方法,用于设置对应logger的级别

SpringBoot在启动时,会完成LoggingSystem的初始化,这部分代码是在LoggingApplicationListener中实现的:

/**
 * 执行LoggingSystem初始化的前置操作
 */
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    //获取LoggingSystem的真实实现,
    // 此处会根据不同的日志框架获取不同的实现,
    // logback : LogbackLoggingSystem
    // log4j2: Log4J2LoggingSystem
    // javalog: JavaLoggingSystem
    this.loggingSystem = LoggingSystem
        .get(event.getSpringApplication().getClassLoader());
    //执行beforeInitialize方法完成初始化前置操作
    this.loggingSystem.beforeInitialize();
}

有了LoggingSystem以后,我们就可以通过他来动态的修改日志级别。他帮我们屏蔽掉了底层的具体日志框架。

除了支持修改ROOT级别的日志以外,还可以支持用户自定义的日志的级别修改。

先定义一个LoggerConfig,用来封装日志的配置:

/**
 * the config of logger
 *
 * @author Hollis
 */
public class LoggerConfig {

    /**
     * the name of the logger
     */
    private String loggerName;

    /**
     * the log level
     *
     * @see LogLevel
     */
    private String level;

    public String getLoggerName() {
        return loggerName;
    }

    public void setLoggerName(String loggerName) {
        this.loggerName = loggerName;
    }

    public String getLevel() {
        return level;
    }

    public void setLevel(String level) {
        this.level = level;
    }
}

接着提供方法动态修改日志级别,代码实现如下:

public void setLoggerLevel(List<LoggerConfig> configList) {

    Optional.ofNullable(configList).orElse(Collections.emptyList()).forEach(
        config -> {
            LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(config.getLoggerName());

            if (loggerConfiguration == null) {
                if (LOGGER.isErrorEnabled()) {
                    LOGGER.error("no loggerConfiguration with loggerName " + config.getLoggerName());
                }
                return;
            }

            if (!supportLevels().contains(config.getLevel())) {
                if (LOGGER.isErrorEnabled()) {
                    LOGGER.error("current Level is not support : " + config.getLevel());
                }
                return;
            }

            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("setLoggerLevel success for logger '" + config.getLoggerName() + "' ,old level is  '"
                    + loggerConfiguration.getEffectiveLevel()
                    + "' , new level is '" + config.getLevel() + "'");
            }
            loggingSystem.setLogLevel(config.getLoggerName(), LogLevel.valueOf(config.getLevel()));
        }
    );
}

以上,根据用户传入的LoggerConfig,修改指定的loggerName对应的loggerLevel。至于LoggerLevel是怎么来的,就可以通过配置的方式传入,比如解析JSON格式的配置或者YML文件等。

如我们可以在配置中心中采用以下配置来控制日志级别,并推送:

[{'loggerName':'com.hollis.degradation.core.logger.LoggerLevelSettingService','level':'WARN'}]

以上配置,会使得loggerName为com.hollis.degradation.core.logger.LoggerLevelSettingService的日志的级别动态修改为WARN,另外,如果配置信息如下:

[{'loggerName':'com.hollis.degradation.core.logger','level':'WARN'}]

那么,就会将以com.hollis.degradation.core.logger这个包路径下面的所有的类为LoggerName的日志输出的级别全都动态修改为WARN。

当然,这个配置也支持配置多个Logger的级别,如果是以下配置内容:

[
  {'loggerName':'com.hollis.degradation.core.logger','level':'WARN'}
  ,{'loggerName':'com.hollis.degradation.core.logger.LoggerLevelSettingService','level':'INFO'}
]

加入代码中有多个日志,他们的定义方法分别为

private static final Logger LOGGER1 = LoggerFactory.getLogger(LoggerLevelSettingService.class);
private static final Logger LOGGER2 = LoggerFactory.getLogger(TestService.class);
private static final Logger LOGGER3 = LoggerFactory.getLogger(DebugService.class);

那么,配置生效后,会使得以上的LOGGER1的输出级别为INFO,而LOGGER2和LOGGER3的级别为WARN。

除此以外,上面的日志级别修改,可能会影响到我们自己这个工具本身的日志输出,所以,我们提供了一个方法,可以直接修改我们自己这个日志服务的日志级别:

public void setDegradationLoggerLevel(String level) {

    LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(
        this.getClass().getName());

    if (loggerConfiguration == null) {
        if (LOGGER.isWarnEnabled()) {
            LOGGER.warn("no loggerConfiguration with loggerName " + level);
        }
        return;
    }

    if (!supportLevels().contains(level)) {
        if (LOGGER.isErrorEnabled()) {
            LOGGER.error("current Level is not support : " + level);
        }
        return;
    }

    if (!loggerConfiguration.getEffectiveLevel().equals(LogLevel.valueOf(level))) {
        loggingSystem.setLogLevel(this.getClass().getName(), LogLevel.valueOf(level));
    }
}

有了以上的LoggerLevelSettingService类以后,基本具备了动态修改日志的能力,接下来就是想办法通过配置中心动态修改日志级别了。

这里面因为不同的配置中心用法不同,我只是拿我们自己的配置中心简单举例:

/**
 * 降级开关注册器
 *
 * @author Hollis
 */
public class DegradationSwitchInitializer implements Listener, InitializingBean {

    //从配置项中读取应用名,方便注册到配置中心
    @Value("${project.name}")
    private String appName;

    @Autowired
    private LoggerLevelSettingService loggerLevelSettingService;

    //配置中心值发生变化会自动回调该方法
    @Override
    public void valueChange(String appName, String nameSpace, String name,
                            String value) {

        if (name.equals(rootLogLevel.name())) {
            loggerLevelSettingService.setRootLoggerLevel(value);
        }

        if (name.equals(logLevelConfig.name())) {
            List<LoggerConfig> loggerConfigs = JSON.parseArray(value, LoggerConfig.class);
            loggerLevelSettingService.setLoggerLevel(loggerConfigs);
        }

        //将降级工具的日志输出级别设置成INFO,保证其日志可以正常输出
        loggerLevelSettingService.setDegradationLoggerLevel("INFO");
    }

    @Override
    public void afterPropertiesSet() {
        //将服务配置到配置中心
        ConfigCenterManager.addListener(this);
        ConfigCenterManager.init(appName, DegradationConfig.class);
    }
}

以上,我们实现了监听配置中心的值的变化,动态修改日志级别。

基本功能就都完成了,接下来可以考虑如何让其他应用快速接入,那就是定义一个Starter,可以方便快速接入。主要代码如下:

先定义一个Configuration类:

/**
 * @author Hollis
 */
@Configuration
@ConditionalOnProperty(prefix = "hollis.degradation", name = "enable", havingValue = "true")
public class HollisDegradationAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(name = "project.name")
    public LoggerLevelSettingService loggerLevelSettingService() {
        return new LoggerLevelSettingService();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(value = LoggerLevelSettingService.class)
    public DegradationSwitchInitializer degradationSwitchInitializer() {
        return new DegradationSwitchInitializer();
    }
}

在这个类里面定义两个bean,并且bean定义的前提是应用中配置了以下两个配置项:

hollis.degradation.enable = true
project.name = test

接下来就是定一个spring.factories文件,定义内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.hollis.degradation.starter.autoconfiguration.HollisDegradationAutoConfiguration

以上,只需要在需要引入降级工具的应用中,引入我们的这个starter,并且配置两个配置项即可。

接入后,可以方便的在配置中心中动态修改单机或者集群的日志输出级别,并且可以在大促期间配置到预案平台上,通过紧急预案快速执行。

以上,基本实现了很多基本的功能,实现时考虑的因素主要有以下几个:

  • 1、通用。要同时可以支持不同的日志框架,客户端使用的日志框架不影响我们的功能,并且客户端不需要关心自己的日志框架的区别。
  • 2、可配置。可以将配置信息通过外部配置中心推送,可以快速进行调整。
  • 3、易用。通过封装到SpringBoot Starter中,方便客户端快速接入。
  • 4、无侵入性。框架的使用不应该影响到应用的正常运行。

当然,这个工具只是我花了几个小时撸出来的,其中还有很多不足,其实还有很多事情可以优化,比如配置的格式可以支持多种、支持通过EndPoint查看日志配置情况等,这些都还没有实现。

本文只是提供一个思路,希望大家都能学会用工具化的方式解决日常工作中遇到的问题,学会造轮子。

(全文完)
扫描二维码,关注作者微信公众号 %E4%BA%8C%E7%BB%B4%E7%A0%81.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK