前言
動态調整線上日志級别是一個非常常見的場景,借助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檔案就可以徹底的删除了