Spring Boot + Apollo 動(dòng)態(tài)修改日志級(jí)別

起因

你是否碰到過如下場(chǎng)景:

  1. 在測(cè)試環(huán)境未發(fā)現(xiàn)的BUG,上了生產(chǎn)環(huán)境之后偶現(xiàn)歇拆,但同樣由于缺少調(diào)試信息,無法定位問題
  2. 調(diào)用內(nèi)部服務(wù)础嫡、第三方服務(wù)氛谜,在某些case下系統(tǒng)未按預(yù)期運(yùn)行掏觉,排查代碼后懷疑是被依賴方返回了錯(cuò)誤的數(shù)據(jù)導(dǎo)致,但苦于打印Response的日志為DEBUG值漫,沒有證據(jù)

在以前的解決方案是澳腹,將日志級(jí)別改成DEBUG并上個(gè)線,排查完問題之后杨何,再將日志級(jí)別改回INFO酱塔,再上一次線,整個(gè)生命周期很長(zhǎng)危虱;又或者為了省事羊娃,直接將調(diào)試日志級(jí)別定為INFO,避免上線埃跷。這兩種方式蕊玷,無論哪種都不夠優(yōu)雅,如果有一種方式弥雹,能夠動(dòng)態(tài)修改日志級(jí)別垃帅,在需要排查問題的時(shí)候改成DEBUG,不需要的時(shí)候就恢復(fù)INFO剪勿,豈不妙哉贸诚?

因此,我們期望實(shí)現(xiàn)的目標(biāo)是:

  1. 能夠動(dòng)態(tài)修改日志級(jí)別并及時(shí)生效(主要目標(biāo))
  2. 在使用上厕吉,與Spring Boot提供的設(shè)置日志級(jí)別方式兼容(不另造輪子酱固,降低學(xué)習(xí)成本)
  3. 問題排查完之后,能夠簡(jiǎn)單還原日志級(jí)別(移除之前的修改)

解決方案

針對(duì)需要實(shí)現(xiàn)的目標(biāo)头朱,逐條分析:

  1. 借助一些工具(如Arthas)媒怯,直接修改運(yùn)行時(shí)內(nèi)存中的值,但是在集群環(huán)境下需要逐臺(tái)修改髓窜,實(shí)施成本較高扇苞;借助配置中心(如Apollo),修改之后由配置中心把數(shù)據(jù)推給應(yīng)用服務(wù)器寄纵,時(shí)延取決于配置中心推送的能力
  2. Spring Boot提供的設(shè)置日志級(jí)別的方式是鳖敷,在application.properties/application.xml里配置logging.level.{loggerName} = DEBUG,希望仍然沿用這種方式
  3. 為了排查問題程拭,將某個(gè)類的日志級(jí)別設(shè)置為DEBUG定踱,完事之后,通過將本Logger日志級(jí)別設(shè)置為父Logger的日志級(jí)別進(jìn)行還原

Spring Boot提供了抽象日志系統(tǒng)(org.springframework.boot.logging.LoggingSystem)恃鞋,通過借助LoggingSystem可以實(shí)現(xiàn)修改日志級(jí)別的目的崖媚,而Apollo則為動(dòng)態(tài)修改提供了可能性

因此亦歉,最終采用的方案為Spring Boot(LoggingSystem) + Apollo,選用的日志組件為Logback為例畅哑,源碼如下:

public class LoggingLevelRefresher {
    private final static Logger log = LoggerFactory.getLogger(LoggingLevelRefresher.class);

    private static final String PREFIX = "logging.level.";
    private static final String ROOT = LoggingSystem.ROOT_LOGGER_NAME;
    private static final String SPLIT = ".";

    @Resource
    private LoggingSystem loggingSystem;

    @ApolloConfig
    private Config config;

    @PostConstruct
    private void init() {
        refreshLoggingLevels(config.getPropertyNames());
    }

    @ApolloConfigChangeListener(interestedKeyPrefixes = PREFIX)
    private void onChange(ConfigChangeEvent changeEvent) {
        refreshLoggingLevels(changeEvent.changedKeys());
    }

    private void refreshLoggingLevels(Set<String> changedKeys) {
        for (String key : changedKeys) {
            // key may be : logging.level.com.example.web
            if (StringUtils.startsWithIgnoreCase(key, PREFIX)) {
                String loggerName = PREFIX.equalsIgnoreCase(key) ? ROOT : key.substring(PREFIX.length());
                String strLevel = config.getProperty(key, parentStrLevel(loggerName));
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
                loggingSystem.setLogLevel(loggerName, level);

                log(loggerName, strLevel);
            }
        }
    }


    private String parentStrLevel(String loggerName) {
        String parentLoggerName = loggerName.contains(SPLIT) ? loggerName.substring(0, loggerName.lastIndexOf(SPLIT)) : ROOT;
        return loggingSystem.getLoggerConfiguration(parentLoggerName).getEffectiveLevel().name();
    }

    /**
     * 獲取當(dāng)前類的Logger對(duì)象有效日志級(jí)別對(duì)應(yīng)的方法肴楷,進(jìn)行日志輸出。舉例:
     * 如果當(dāng)前類的EffectiveLevel為WARN荠呐,則獲取的Method為 `org.slf4j.Logger#warn(java.lang.String, java.lang.Object, java.lang.Object)`
     * 目的是為了輸出`changed {} log level to:{}`這一行日志
     */
    private void log(String loggerName, String strLevel) {
        try {
            LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(log.getName());
            Method method = log.getClass().getMethod(loggerConfiguration.getEffectiveLevel().name().toLowerCase(), String.class, Object.class, Object.class);
            method.invoke(log, "changed {} log level to:{}", loggerName, strLevel);
        } catch (Exception e) {
            log.error("changed {} log level to:{} error", loggerName, strLevel, e);
        }
    }
}

能夠?qū)崿F(xiàn)的效果如下:

  1. Apollo配置logging.level.com.example.web = DEBUG赛蔫,能夠?qū)oggerName為com.example.web的日志級(jí)別改成DEBUG, 一般情況下,loggerName也等同于包名泥张,也即是該包下的類日志級(jí)別都會(huì)被改成DEBUG(使用方式同等于在application.properties里的配置)
  2. Apollo里刪掉logging.level.com.example.web的配置項(xiàng)呵恢,系統(tǒng)會(huì)將com.example.web的日志級(jí)別設(shè)置為等于同com.example的日志級(jí)別,默認(rèn)情況下com.example等同于ROOT的日志級(jí)別媚创,也就是INFO渗钉,就達(dá)到了恢復(fù)的目的

原理分析

源碼基于Spring Boot 2.1.10.RELEASE

  1. Spring Boot應(yīng)用啟動(dòng),運(yùn)行到Spring容器的生命周期節(jié)點(diǎn)(擴(kuò)展點(diǎn))時(shí)钞钙,Spring會(huì)發(fā)出一些通知事件鳄橘,例如ApplicationStartingEventApplicationEnvironmentPreparedEvent歇竟、ApplicationPreparedEvent等等挥唠,讓我們可以有機(jī)會(huì)監(jiān)聽這些事件抵恋,并且搞事情焕议。Spring 內(nèi)部也定義了一系列監(jiān)聽器,用于監(jiān)聽生命周期事件弧关,來進(jìn)行擴(kuò)展(思想:微內(nèi)核 + 插件)盅安。
    如下圖所示,Spring Boot內(nèi)部定義了org.springframework.boot.context.logging.LoggingApplicationListener世囊,并且監(jiān)聽了ApplicationStartingEvent事件别瞭,在事件中,構(gòu)造了日志系統(tǒng)loggingSystem株憾,并且執(zhí)行初始化之前的回調(diào)蝙寨,為初始化做準(zhǔn)備
image.png

我們接著看org.springframework.boot.logging.LoggingSystem#get(java.lang.ClassLoader)

image.png

通過代碼,我們知道有兩種方式指定底層日志組件

    1. 通過環(huán)境變量指定嗤瞎。例如下述方式指定了Logback做為底層日志組件
-Dorg.springframework.boot.logging.LoggingSystem=org.springframework.boot.logging.logback.LogbackLoggingSystem
    1. Spring Boot預(yù)定義的日志系統(tǒng)順序查找墙歪,排在前面的日志組件優(yōu)先級(jí)高
image.png

可以看到,LoggingSystem支持的日志組件贝奇,按順序有如下三種

  • Logback
  • Log4j2
  • jul(java.util.logging)

一般情況下我們不會(huì)手動(dòng)指定環(huán)境變量虹菲,而是采用一種約定優(yōu)于配置的思想,交由Spring Boot判斷:只要存在Logback相關(guān)類掉瞳,就認(rèn)為Logback應(yīng)該生效作為底層的日志組件毕源,其它的依此類推浪漠。

源碼從側(cè)面也透漏著一個(gè)信息:Spring Boot偏愛Logback

我們這里就以Logback為例,因此霎褐,此時(shí)應(yīng)用激活的是org.springframework.boot.logging.logback.LogbackLoggingSystem

  1. 系統(tǒng)繼續(xù)運(yùn)行址愿,Spring會(huì)發(fā)出ApplicationEnvironmentPreparedEvent事件,并且仍由LoggingApplicationListener進(jìn)行監(jiān)聽瘩欺,在監(jiān)聽時(shí)進(jìn)行了日志組件的初始化必盖,如此,一個(gè)日志系統(tǒng)(LoggingSystem)便構(gòu)造完畢
image.png
image.png
  1. Apollo添加logging.level.{loggerName} = DEBUG的配置項(xiàng)俱饿,會(huì)觸發(fā)應(yīng)用去Apollo拉取最新的配置信息歌粥,并且將變更內(nèi)容進(jìn)行回調(diào)。在回調(diào)事件中拍埠,通過獲取配置的日志級(jí)別失驶,調(diào)用LoggingSystem#setLogLevel方法調(diào)整對(duì)應(yīng)logger的日志級(jí)別;刪除該配置項(xiàng)枣购,同樣會(huì)觸發(fā)應(yīng)用去Apollo拉取最新的配置信息嬉探,changedKeys包含刪掉的配置項(xiàng),此時(shí)調(diào)用Config#getProperty必然獲取不到配置項(xiàng)的信息(因?yàn)橐呀?jīng)刪除)棉圈,因此getProperty第二個(gè)參數(shù)就是用于指定當(dāng)獲取的配置項(xiàng)值為null時(shí)的默認(rèn)值涩堤。此處,我們獲取了父Logger的Level作為默認(rèn)值分瘾,便達(dá)到了恢復(fù)的目的胎围。此處需要注意的一點(diǎn)是,如果照搬源碼德召,使用的日志組件一定得Logback白魂,緣由是在獲取父Logger的EffectiveLevel實(shí)現(xiàn)方式上取了巧,如果使用的是Log4j2上岗,會(huì)出現(xiàn)空指針異常---->究其原因福荸,日志組件底層實(shí)現(xiàn)機(jī)制不同,行為也就不一樣
image.png

總結(jié)

  1. Spring Boot 在構(gòu)建Spring容器的生命過程中肴掷,初始化了日志系統(tǒng)LoggingSystem敬锐,并和某種日志組件如Logback進(jìn)行了綁定。如此呆瞻,通過LoggingSystem暴露出來的setLogLevel接口台夺,屏蔽了不同日志組件之間的差異,忽略底層日志組件存在的同時(shí)栋烤,又能在需要時(shí)刻調(diào)用接口修改日志級(jí)別(抽象的魅力)
  2. 借助配置中心(如Apollo)的推送能力谒养,應(yīng)用能夠準(zhǔn)實(shí)時(shí)獲取所配置的Logger日志級(jí)別,并調(diào)用LoggingSystem#setLogLevel進(jìn)行日志級(jí)別的設(shè)置

題外話

  1. 本文雖借助Spring Boot的日志系統(tǒng)機(jī)制,但本質(zhì)上也是委托給底層的日志組件來實(shí)現(xiàn)的买窟,也就是說丰泊,即便非Spring Boot應(yīng)用,同樣能夠修改日志級(jí)別始绍。我們需要具備發(fā)散思維的能力瞳购,知其然,并知其所以然亏推。另一方面学赛,即便擁有這樣的能力,在Spring Boot環(huán)境下吞杭,仍然不建議直接訪問日志組件設(shè)置日志級(jí)別的API盏浇,應(yīng)該擁抱Spring Boot的生態(tài),借助其對(duì)日志的抽象能力芽狗,面向接口編程绢掰,而不是面向?qū)崿F(xiàn)編程
  2. 本文雖借助Apollo來實(shí)現(xiàn)動(dòng)態(tài)修改的能力,但實(shí)際上童擎,能實(shí)現(xiàn)此能力的組件依然很多滴劲,例如ZKNacos顾复、Spring Cloud Config + Spring Cloud Bus等等班挖,在業(yè)務(wù)開發(fā)中,依賴這類基礎(chǔ)組件是很正常的事情芯砸,這取決于公司的技術(shù)選型萧芙,相應(yīng)改造一下方案即可
  3. 本文雖以Logback為日志組件貫徹全文,但對(duì)于Log4j2以及jul仍然適用乙嘀,對(duì)于一個(gè)日志組件而言末购,設(shè)置日志級(jí)別是最基本的功能之一破喻。因此虎谢,可以根據(jù)公司的技術(shù)規(guī)范來確定日志組件,如無統(tǒng)一標(biāo)準(zhǔn)曹质,建議跟著Spring Boot走婴噩,畢竟Log4j2出道6年,Spring Boot也從1.x到2.x羽德,但仍然偏愛Logback几莽,心里就沒點(diǎn)數(shù)么(雖說log4j2快,但fastjson也很快宅静,敢用否)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末章蚣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子姨夹,更是在濱河造成了極大的恐慌纤垂,老刑警劉巖矾策,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異峭沦,居然都是意外死亡贾虽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門吼鱼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蓬豁,“玉大人,你說我怎么就攤上這事菇肃〉胤啵” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵琐谤,是天一觀的道長(zhǎng)驶忌。 經(jīng)常有香客問我,道長(zhǎng)笑跛,這世上最難降的妖魔是什么付魔? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮飞蹂,結(jié)果婚禮上几苍,老公的妹妹穿的比我還像新娘。我一直安慰自己陈哑,他們只是感情好妻坝,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惊窖,像睡著了一般刽宪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上界酒,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天圣拄,我揣著相機(jī)與錄音,去河邊找鬼毁欣。 笑死庇谆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凭疮。 我是一名探鬼主播饭耳,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼执解!你這毒婦竟也來了寞肖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎新蟆,沒想到半個(gè)月后耕姊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡栅葡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年茉兰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欣簇。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡规脸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出熊咽,到底是詐尸還是另有隱情莫鸭,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布横殴,位于F島的核電站被因,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏衫仑。R本人自食惡果不足惜梨与,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望文狱。 院中可真熱鬧粥鞋,春花似錦、人聲如沸瞄崇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苏研。三九已至等浊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摹蘑,已是汗流浹背筹燕。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纹蝴,地道東北人庄萎。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓踪少,卻偏偏與公主長(zhǎng)得像塘安,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子援奢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容