集成apollo動態日誌,「消滅」logback-spring.xml

前言

動態調整線上日誌級別是一個很是常見的場景,藉助apollo這種配置中心組件很是容易實現。做爲apollo的官方技術支持,博主常常在技術羣看到有使用者詢問apollo是否能夠託管logback的配置文件,畢竟有了配置中心後,消滅全部的本地配置所有交給apollo管理是咱們的最終目標。但是,apollo不具有直接託管logback-spring.xml配置文件能力,可是,咱們能夠基於spring和logback的裝載機制,徹底取締logback-spring.xml配置,以apollo中的配置驅動。並且,改造後,大大提升了日誌系統的靈活性和可擴展性。html

apollo動態日誌

何爲apollo動態日誌?直接這樣說可能會有歧義,覺得是apollo裏的日誌,其實否則。舉個簡單的例子,好比,咱們項目不少地方使用了log.debug()打印日誌,爲了方便經過日誌信息排查問題,可是通常狀況下,生產環境的日誌級別會配置成info。只有遇到須要排查線上問題的時候纔會臨時打開debug級別日誌。這個時候只能需改配置文件,將日誌級別調整成debug,而後從新打包部署驗證。不只流程繁瑣耗時,還會破壞當時的"案發現場的環境",致使判斷不許確。若是應用具有了apollo動態日誌這種能力,就只需在apollo修改下配置而後提交,就能夠熱更新日誌級別,立刻打印debug級別日誌。這就是所謂的apollo動態日誌。實現這個效果,須要具有兩個能力,分別由spring和apollo提供java

spring日誌系統熱更新日誌級別

spring應用中,spring適配了主流的日誌框架,如logback、log4j2等,在這些日誌框架之上,又抽象了本身的日誌系統服務,這裏咱們用到了spring的LoggingSystem,用它來熱更新日誌級別,這個類在日誌系統初始化時就添加到了spring的容器中,因此只要在spring的上下文管理範圍內,就能夠直接注入,如下爲主要使用到的api描述:web

/**
     * 設置給定日誌記錄器的日誌級別.
     * @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實例手動添加,如:spring

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

也能夠經過註解直接驅動apache

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

實現日誌調整熱更新

有了上述能力,在結合spring支持的日誌加載配置方式,如:api

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

能夠實現以下代碼完成功能,遇到須要調整日誌級別時,修改apollo裏的配置,便可實時生效session

@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配置以前,先看下這個配置文件有哪些配置信息,起到了哪些做用,下面貼出一個典型的配置文件內容:app

<?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、log4j二、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文件就能夠完全的刪除了

相關文章
相關標籤/搜索