求求你,別再隨便打日志了,教你動(dòng)態(tài)修改日志級(jí)別!
???
作者 l Hollis來(lái)源 l Hollis(ID:hollischuang)
之前寫過(guò)一篇文章《服務(wù)被干爆了!竟然是日志的鍋》,介紹過(guò)一次大促故障,是因?yàn)槿罩玖考ぴ觯瑢?dǎo)致服務(wù)器差點(diǎn)掛掉。在那次問(wèn)題發(fā)生之后,我開發(fā)了一個(gè)簡(jiǎn)單的日志降級(jí)的小工具,通過(guò)配置的方式,動(dòng)態(tài)推送日志級(jí)別,動(dòng)態(tài)修改線上的日志輸出級(jí)別。并且把這份配置的修改配置到我們的預(yù)案平臺(tái)上,大促期間進(jìn)行定時(shí)或者緊急預(yù)案處理。那么,這篇文章就來(lái)簡(jiǎn)單介紹下思路以及代碼實(shí)現(xiàn)。
日志級(jí)別在開始正文前簡(jiǎn)單介紹下日志級(jí)別,不同的日志框架支持不同的日志級(jí)別,其中比較常見的就是Log4j和Logback。在Log4j中支持8種日志級(jí)別,優(yōu)先級(jí)從高到低依次為:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。Logback中支持7種日志級(jí)別,優(yōu)先級(jí)從高到低分別是:OFF、ERROR、WARN、INFO、DEBUG、TRACE、ALL。可以看到常見的ERROR、WARN、INFO、DEBUG,這兩者都是支持的。所謂設(shè)置日志的輸出級(jí)別表示的是輸出的日志的最低級(jí)別,也就是說(shuō),如果我們把級(jí)別設(shè)置成INFO,那么包括INFO在內(nèi)以及比INFO優(yōu)先級(jí)高的級(jí)別的日志都可以輸出。無(wú)論是Log4j還是Logback,都是通過(guò)日志的配置文件來(lái)控制日志輸出級(jí)別的。這里就不詳述了。
日志框架上面我們提到了Log4j和Logback,這兩種都是比較常用的日志框架。但是很多時(shí)候,我們?cè)诖a中打印日志并不是直接使用這種日志框架來(lái)進(jìn)行的,而是依賴了一個(gè)日志門面來(lái)進(jìn)行的,如slf4j、commons-logging等。一般最最常用的方法就是通過(guò)slf4j提供的LoggerFactory的getLogger來(lái)獲取Logger,然后進(jìn)行日志打印
private?static?final?Logger?LOGGER?=?LoggerFactory.getLogger(LoggerService.class);當(dāng)我們使用LoggerFactory.getLogger方法創(chuàng)建一個(gè)Logger對(duì)象的時(shí)候,會(huì)給他傳入一個(gè)loggerName,通過(guò)這個(gè)loggerName來(lái)唯一識(shí)別一個(gè)Logger,如上面的方式就是使用LoggerService這個(gè)類的全路徑名作為其loggerName。loggerName是每一個(gè)Logger的配置信息一部分,除此之外還有日志輸出級(jí)別等信息。關(guān)于為什么不直接使用log4j和logback打印日志,我在《為什么阿里巴巴禁止工程師直接使用日志系統(tǒng)(Log4j、Logback)中的 API》中分析過(guò)。
public?void?test(){
????LOGGER.info("hollis?log?test");
}
Arthas改變?nèi)罩炯?jí)別在開始介紹代碼實(shí)現(xiàn)之前,先介紹一個(gè)工具,也可以幫助我們的動(dòng)態(tài)修改日志級(jí)別。那就是阿里開源的神器——Arthas (https://arthas.aliyun.com/doc/ )。Arthas提供了一個(gè)logger命令,這個(gè)命令可以查看和更新logger信息,包括日志級(jí)別。查看指定名字的logger信息
[arthas@2062]$?logger?-n?org.springframework.web更新logger level
?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
[arthas@2062]$?logger?--name?ROOT?--level?debug簡(jiǎn)單吧,使用一個(gè)命令就可以修改機(jī)日志級(jí)別了。但是Arthas目前對(duì)于集群的支持并不是特別的友好,雖然他支持了通過(guò)Arthas Tunnel Server/Client 來(lái)遠(yuǎn)程管理/連接多個(gè)Agent,但是使用起來(lái)還不是很方便,并且對(duì)于命令的使用要求比較高。還有就是我們系統(tǒng)通過(guò)一個(gè)工具,方便我們?cè)诖蟠倨陂g通過(guò)預(yù)案方式動(dòng)態(tài)調(diào)整日志級(jí)別,這方面使用arthas就不是很方便了。
update?logger?level?success.
代碼實(shí)現(xiàn)我寫的這個(gè)工具功能很簡(jiǎn)單,就是提供動(dòng)態(tài)修改日志級(jí)別的入口,方便用戶動(dòng)態(tài)修改級(jí)別。并且為了方便使用,我將他封裝在一個(gè)Spring Boot Starter里面了,還有就是將他直接對(duì)接到公司內(nèi)部的配置中心中,可以方便的通過(guò)配置中心一鍵修改日志級(jí)別。首先看下其中最核心的功能,那就是動(dòng)態(tài)修改日志級(jí)別的部分,代碼如下
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;
/**
?*?日志級(jí)別設(shè)置服務(wù)類
?*
?*?@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
????????return?loggingSystem.getSupportedLogLevels().stream().map(Enum::name).collect(Collectors.toList());
????}
}
?通過(guò)上圖,我們可以發(fā)現(xiàn)目前SpringBoot目前支持4種類型的日志,分別是JDK內(nèi)置的Log(JavaLoggingSystem)以及Log4j(Log4JLoggingSystem)、Log4j2(Log4J2LoggingSystem)以及Logback(LogbackLoggingSystem)。LoggingSystem是個(gè)抽象類,內(nèi)部有這幾個(gè)方法:beforeInitialize方法:日志系統(tǒng)初始化之前需要處理的事情。抽象方法,不同的日志架構(gòu)進(jìn)行不同的處理
initialize方法:初始化日志系統(tǒng)。默認(rèn)不進(jìn)行任何處理,需子類進(jìn)行初始化工作
cleanUp方法:日志系統(tǒng)的清除工作。默認(rèn)不進(jìn)行任何處理,需子類進(jìn)行清除工作
getShutdownHandler方法:返回一個(gè)Runnable用于當(dāng)jvm退出的時(shí)候處理日志系統(tǒng)關(guān)閉后需要進(jìn)行的操作,默認(rèn)返回null,也就是什么都不做
setLogLevel方法:抽象方法,用于設(shè)置對(duì)應(yīng)logger的級(jí)別
/**有了LoggingSystem以后,我們就可以通過(guò)他來(lái)動(dòng)態(tài)的修改日志級(jí)別。他幫我們屏蔽掉了底層的具體日志框架。除了支持修改ROOT級(jí)別的日志以外,還可以支持用戶自定義的日志的級(jí)別修改,代碼實(shí)現(xiàn)如下:先定義一個(gè)LoggerConfig,用來(lái)封裝日志的配置
?*?執(zhí)行LoggingSystem初始化的前置操作
?*/
private?void?onApplicationStartingEvent(ApplicationStartingEvent?event)?{
????//獲取LoggingSystem的真實(shí)實(shí)現(xiàn),
????//?此處會(huì)根據(jù)不同的日志框架獲取不同的實(shí)現(xiàn),
????// logback :LogbackLoggingSystem
????// log4j2:Log4J2LoggingSystem
????// javalog:JavaLoggingSystem
????this.loggingSystem?=?LoggingSystem
????????.get(event.getSpringApplication().getClassLoader());
????//執(zhí)行beforeInitialize方法完成初始化前置操作
????this.loggingSystem.beforeInitialize();
}
/**
?*?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;
????}
}
接著提供方法動(dòng)態(tài)修改日志級(jí)別:
public?void?setLoggerLevel(List以上,根據(jù)用戶傳入的LoggerConfig,修改指定的loggerName對(duì)應(yīng)的loggerLevel。至于LoggerLevel是怎么來(lái)的,就可以通過(guò)配置的方式傳入,比如解析JSON格式的配置或者YML文件等。如我們可以在配置中心中采用以下配置來(lái)控制日志級(jí)別,并推送:?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()));
????????}
????);
}
[{'loggerName':'com.hollis.degradation.core.logger.LoggerLevelSettingService','level':'WARN'}]
以上配置,會(huì)使得loggerName為com.hollis.degradation.core.logger.LoggerLevelSettingService的日志的級(jí)別動(dòng)態(tài)修改為WARN,另外,如果配置信息如下:[{'loggerName':'com.hollis.degradation.core.logger','level':'WARN'}]
那么,就會(huì)將以com.hollis.degradation.core.logger這個(gè)包路徑下面的所有的類為L(zhǎng)oggerName的日志輸出的級(jí)別全都動(dòng)態(tài)修改為WARN。當(dāng)然,這個(gè)配置也支持配置多個(gè)Logger的級(jí)別,如果是以下配置內(nèi)容:[加入代碼中有多個(gè)日志,他們的定義方法分別為
??{'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);那么,配置生效后,會(huì)使得以上的LOGGER1的輸出級(jí)別為INFO,而LOGGER2和LOGGER3的級(jí)別為WARN。除此以外,上面的日志級(jí)別修改,可能會(huì)影響到我們自己這個(gè)工具本身的日志輸出,所以,我們提供了一個(gè)方法,可以直接修改我們自己這個(gè)日志服務(wù)的日志級(jí)別
private?static?final?Logger?LOGGER2?=?LoggerFactory.getLogger(TestService.class);
private?static?final?Logger?LOGGER3?=?LoggerFactory.getLogger(DebugService.class);
public?void?setDegradationLoggerLevel(String?level)?{有了以上的LoggerLevelSettingService類以后,基本具備了動(dòng)態(tài)修改日志的能力,接下來(lái)就是想辦法通過(guò)配置中心動(dòng)態(tài)修改日志級(jí)別了。這里面因?yàn)椴煌呐渲弥行挠梅ú煌抑皇悄梦覀冏约旱呐渲弥行暮?jiǎn)單舉例
????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));
????}
}
/**以上,我們實(shí)現(xiàn)了監(jiān)聽配置中心的值的變化,動(dòng)態(tài)修改日志級(jí)別。基本功能就都完成了,接下來(lái)可以考慮如何讓其他應(yīng)用快速接入,那就是定義一個(gè)Starter,可以方便快速接入。主要代碼如下:先定義一個(gè)Configuration類:
?*?降級(jí)開關(guān)注冊(cè)器
?*
?*?@author?Hollis
?*/
public?class?DegradationSwitchInitializer?implements?Listener,?InitializingBean?{
????//從配置項(xiàng)中讀取應(yīng)用名,方便注冊(cè)到配置中心
????@Value("${project.name}")
????private?String?appName;
????@Autowired
????private?LoggerLevelSettingService?loggerLevelSettingService;
????//配置中心值發(fā)生變化會(huì)自動(dòng)回調(diào)該方法
????@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?loggerConfigs?=?JSON.parseArray(value,?LoggerConfig.class);
????????????loggerLevelSettingService.setLoggerLevel(loggerConfigs);
????????}
????????//將降級(jí)工具的日志輸出級(jí)別設(shè)置成INFO,保證其日志可以正常輸出
????????loggerLevelSettingService.setDegradationLoggerLevel("INFO");
????}
????@Override
????public?void?afterPropertiesSet()?{
????????//將服務(wù)配置到配置中心
????????ConfigCenterManager.addListener(this);
????????ConfigCenterManager.init(appName,?DegradationConfig.class);
????}
}
/**在這個(gè)類里面定義兩個(gè)bean,并且bean定義的前提是應(yīng)用中配置了以下兩個(gè)配置項(xiàng):
?*?@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();
????}
}
hollis.degradation.enable?=?true接下來(lái)就是定一個(gè)spring.factories文件,定義內(nèi)容如下:
project.name?=?test
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.hollis.degradation.starter.autoconfiguration.HollisDegradationAutoConfiguration以上,只需要在需要引入降級(jí)工具的應(yīng)用中,引入我們的這個(gè)starter,并且配置兩個(gè)配置項(xiàng)即可。接入后,可以方便的在配置中心中動(dòng)態(tài)修改單機(jī)或者集群的日志輸出級(jí)別,并且可以在大促期間配置到預(yù)案平臺(tái)上,通過(guò)緊急預(yù)案快速執(zhí)行。
代碼實(shí)現(xiàn)以上,基本實(shí)現(xiàn)了很多基本的功能,實(shí)現(xiàn)時(shí)考慮的因素主要有以下幾個(gè):
1、通用性。要同時(shí)可以支持不同的日志框架,客戶端使用的日志框架不影響我們的功能,并且客戶端不需要關(guān)心自己的日志框架的區(qū)別。
2、可配置性??梢詫⑴渲眯畔⑼ㄟ^(guò)外部配置中心推送,可以快速進(jìn)行調(diào)整。
3、易用性。通過(guò)封裝到SpringBoot Starter中,方便客戶端快速接入。
4、無(wú)侵入性??蚣艿氖褂貌粦?yīng)該影響到應(yīng)用的正常運(yùn)行。
