乐趣区

关于apollo:集成apollo动态日志消灭logbackspringxml

前言

动静调整线上日志级别是一个十分常见的场景,借助 apollo 这种配置核心组件非常容易实现。作为 apollo 的官网技术支持,博主常常在技术群看到有使用者询问 apollo 是否能够托管 logback 的配置文件,毕竟有了配置核心后,毁灭所有的本地配置全副交给 apollo 治理是咱们的最终目标。可是,apollo 不具备间接托管 logback-spring.xml 配置文件能力,然而,咱们能够基于 spring 和 logback 的装载机制,齐全取缔 logback-spring.xml 配置,以 apollo 中的配置驱动。而且,革新后,大大提高了日志零碎的灵活性和可扩展性。

apollo 动静日志

何为 apollo 动静日志?间接这样说可能会有歧义,认为是 apollo 里的日志,其实不然。举个简略的例子,比方,咱们我的项目很多中央应用了 log.debug()打印日志,为了不便通过日志信息排查问题,然而个别状况下,生产环境的日志级别会配置成 info。只有遇到须要排查线上问题的时候才会长期关上 debug 级别日志。这个时候只能需改配置文件,将日志级别调整成 debug,而后从新打包部署验证。不仅流程繁琐耗时,还会毁坏过后的 ” 案发现场的环境 ”,导致判断不精确。如果利用具备了 apollo 动静日志这种能力,就只需在 apollo 批改下配置而后提交,就能够热更新日志级别,马上打印 debug 级别日志。这就是所谓的 apollo 动静日志。实现这个成果,须要具备两个能力,别离由 spring 和 apollo 提供

spring 日志零碎热更新日志级别

spring 利用中,spring 适配了支流的日志框架,如 logback、log4j2 等,在这些日志框架之上,又形象了本人的日志零碎服务,这里咱们用到了 spring 的 LoggingSystem,用它来热更新日志级别,这个类在日志零碎初始化时就增加到了 spring 的容器中,所以只有在 spring 的上下文治理范畴内,就能够间接注入,以下为次要应用到的 api 形容:

    /**
     * 设置给定日志记录器的日志级别.
     * @param loggerName 要设置的日志记录器的名称 ({@code null} 可用于根日志记录器)。* @param level 日志级别
     */
    public void setLogLevel(String loggerName, LogLevel level) {throw new UnsupportedOperationException("Unable to set log level");
    }

apollo 日志配置变更动静下发

apollo 作为分布式配置核心,配置集中管理和配置热更新是其最外围的性能,此外,apollo 还提供了配置变更下发监听的性能。基于这个配置监听的设计,实现动静日志就变得非常简单了。而且不仅能够实现日志动静热更,基于这个思路,连接池、数据源等都能够轻松实现。apollo 实现监听配置变更有多种形式,能够通过 Config 实例手动增加,如:

    @ApolloConfig
    public Config config;
    
    public void addConfigChangeListener(){
        config.addChangeListener(changeEvent->{System.out.println("config change keys" + changeEvent.changedKeys());
        });
    }

也能够通过注解间接驱动

    @ApolloConfigChangeListener
    public void addConfigChangeListener(ConfigChangeEvent changeEvent){System.out.println("config change keys" + changeEvent.changedKeys());
    }

实现日志调整热更新

有了上述能力,在联合 spring 反对的日志加载配置形式,如:

logging.level.org.springframework.web=debug
logging.level.org.hibernate=error

能够实现如下代码实现性能,遇到须要调整日志级别时,批改 apollo 里的配置,即可实时失效

@Configuration
public class LogbackConfiguration {private static final Logger logger = LoggerFactory.getLogger(LoggerConfiguration.class);
    private static final String LOGGER_TAG = "logging.level.";
    private final LoggingSystem loggingSystem;
    public LogbackConfiguration(LoggingSystem loggingSystem) {this.loggingSystem = loggingSystem;}

    @ApolloConfigChangeListener
    private void onChange(ConfigChangeEvent changeEvent) {for (String key : changeEvent.changedKeys()) {if (this.containsIgnoreCase(key, LOGGER_TAG)) {String strLevel = changeEvent.getChange(key).getNewValue();
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
                loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
                logger.info("logging changed: {},oldValue:{},newValue:{}", key, changeEvent.getChange(key).getOldValue(), strLevel);
            }
        }
    }
    
    private 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;
    }
}

毁灭 logback-spring.xml 配置

在 ” 毁灭 ”logback-xml 配置之前,先看下这个配置文件有哪些配置信息,起到了哪些作用,上面贴出一个典型的配置文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
  <appender name="Sentry" class="io.sentry.logback.SentryAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>ERROR</level>
    </filter>
  </appender>
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="Sentry"/>
  </root>
  <logger name="org.apache.ibatis.session" level="WARN"/>
  <springProfile name="dev">
    <logger name="com.taptap.server" level="DEBUG"/>
    <logger name="com.taptap.commons" level="DEBUG"/>
  </springProfile>
  <springProfile name="prod">
    <logger name="com.taptap.server" level="WARN"/>
    <logger name="com.taptap.commons" level="WARN"/>
  </springProfile>
</configuration>

一个典型的 logback 配置文件里蕴含了 Appender 和日志级别设置的信息,Appender 能够了解为日志的输入源。如上贴出的这个配置,增加了两个 Appender 信息,一个是 spring 中内置的,将日志输入到控制台的 Appender。一个是将 error 日志信息发送到 Sentry 利用监控平台的 Appender。其余的配置形容了每个包门路不同的日志级别信息。到这里,咱们很容易想到,上文曾经说过,spring 曾经反对以 logging.level. 包名 =info 这种配置来设置日志零碎的日志级别。那么剩下的只有解决 Appender 的配置就 ok 了。在这里,其实只须要解决 SentryAppender 的加载就行,因为 consoleAppender spring 本人会解决。有了指标和方向,就好办了。以 logback-spring.xml 配置的信息,最终都会加载成 class 对象。就和 spring.xml 配置一样。所以钻研的方向就变成了 Logback 的加载原理的问题。

Logback 加载原理

在 java 的日志生态里,除了响当当的 logback、log4j2、apache common log 外,还有一个日志框架不得不提,就是 sl4j。正因为 java 生态弱小,日志框架层出不穷,所以 sl4j 进去了,不干实事,专门定义日志规范、标准定义接口。而且,在咱们平时的编码过程中,也倡议应用 sl4j 的 api,这样,无论底层日志框架实现怎么切换,都不会影响。支流的日志框架都有实现 sl4j 的接口,spring 中日志零碎的加载也是面向的 sl4j,而不是间接面向日志实现,加载过程是一个自动化的过程,零碎会主动扫描实现了 sl4j 的接口实现,如:

public interface ILoggerFactory {public Logger getLogger(String name);
}

每个日志框架都会实现这个接口,如 Logback 中的 LoggerContext。Logback 所有的性能都集成在了这个 Context 中,logback-spring.xml 的配置也是为了配置 LoggerContext 中的属性信息,所有咱们只有拿到了 LoggerContext 实例,问题就解决了一大半。这波及到 sl4j 的另一个接口,获取 ILoggerFactory 实例的接口:

public interface LoggerFactoryBinder {public ILoggerFactory getLoggerFactory();

    public String getLoggerFactoryClassStr();}

Logback 的实现类为 StaticLoggerBinder,也就是说,咱们能够通过 StaticLoggerBinder 的 getLoggerFactory 办法拿到 LoggerContext 实例了。

javaBean 加载 SentryAppender

拿到 Logback 的 LoggerContext 后,就好办了,见代码:

@Configuration
public class LogbackConfiguration {private final LoggerContext ctx = (LoggerContext) StaticLoggerBinder.getSingleton().getLoggerFactory();

    @Bean
    @Profile(PROD_ENV)
    public void initSenTry() {SentryAppender sentryAppender = new SentryAppender();
        sentryAppender.setContext(ctx);
        ThresholdFilter filter = new ThresholdFilter();
        filter.setLevel(Level.ERROR.levelStr);
        filter.start();
        sentryAppender.addFilter(filter);
        sentryAppender.start();
        ctx.addTurboFilter(new TurboFilter() {
            @Override
            public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format, Object[] params, Throwable t) {logger.addAppender(sentryAppender);
                return FilterReply.NEUTRAL;
            }
        });
    }
}

看到这种代码就十分有感觉了,配置文件中的 xml 其实就是形容了日志组成对象以及对象的属性。在应用 java bean 的形式配置时须要留神,Logback 的设计里,每个日志零碎组成实例都有一个 start 状态属性,下面的 start()办法其实不是动作,只是标记了这个属性为 true。而在 xml 里这个属性只有配置了就主动激活为 true 了,这里必须显示的 start()一下。解决了日志级别配置和 Appender 配置后,Logback-spring.xml 文件就能够彻底的删除了

退出移动版