背景
作為程序猿,定位問(wèn)題是我們的日常工作,而日志是我們定位問(wèn)題非常重要的依據(jù)。傳統(tǒng)方式定位問(wèn)題時(shí),往往是如下步驟:
- 將日志級(jí)別設(shè)低,例如 DEBUG ;
- 重啟應(yīng)用;
- 復(fù)現(xiàn)問(wèn)題,觀察日志;
那么問(wèn)題就來(lái)了,可不可以動(dòng)態(tài)修改日志級(jí)別呢?(無(wú)需重啟應(yīng)用,就能立刻刷新)
答案是肯定的!
下面提供幾個(gè)思路給大家參考。
使用 LoggingSystem 自行開(kāi)發(fā)修改日志級(jí)別的接口
不廢話,直接上代碼
@Resource
private LoggingSystem loggingSystem;
@PostMApping("/changeLogLevel")
public void changeLogLevel(@RequestParam("name") String name, @RequestParam("level") String level) {
LogLevel logLevel = LogLevel.valueOf(level.toUpperCase());
loggingSystem.setLogLevel(name, logLevel);
}
what?這么簡(jiǎn)單?是的,就是這么簡(jiǎn)單。
LoggingSystem 這個(gè)抽象類(lèi)就是關(guān)鍵,其實(shí)后面所要介紹的幾個(gè)修改思路(actuator,Apollo,mq)的底層也是基于它進(jìn)行修改的。
如果大家對(duì)LoggingSystem這個(gè)類(lèi)在底層究竟是如何實(shí)現(xiàn)動(dòng)態(tài)修改日志級(jí)別感興趣的話,請(qǐng)?jiān)u論區(qū)留言,我抽時(shí)間再寫(xiě)一篇文章來(lái)詳細(xì)說(shuō)一下。
然后再說(shuō)一下這種方式的優(yōu)缺點(diǎn)吧。
優(yōu)點(diǎn):簡(jiǎn)單!
缺點(diǎn):也很明顯,只適合單機(jī)/生產(chǎn)機(jī)器不多的服務(wù)。如果你的服務(wù)有上百個(gè)節(jié)點(diǎn),用這種方式來(lái)修改。。。
那有朋友會(huì)問(wèn),有沒(méi)有適合多機(jī)集群的服務(wù)的修改方式?
那必須有啊,下面介紹一下思路二。
使用 Apollo + LoggingSystem
這種方式的前提是系統(tǒng)接入了Apollo。
也不廢話,直接上代碼吧。代碼里也有注釋。
@Configuration
public class LogLevelRefresher {
private final static Logger log = LoggerFactory.getLogger(com.dylan.config.LoggingLevelRefresher.class);
private static final String PREFIX = "logging.level.";
private static final String ROOT = LoggingSystem.ROOT_LOGGER_NAME;
@Resource
private LoggingSystem loggingSystem;
/**
* 支持類(lèi)配置
*/
@PostConstruct
private void init() {
//要修改日志級(jí)別的key(包路徑/類(lèi)路徑)
String keyStr = ConfigCenterService.getAppProperty("log.changeKey", "logging.level.root,logging.level.com.dylan.config");
Set<String> changedKeys = Arrays.stream(keyStr.split(",")).collect(Collectors.toSet());
refreshLoggingLevels(changedKeys);
}
/**
* 修改Apollo配置后的回調(diào)方法
*/
@ApolloConfigChangeListener
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 = ConfigCenterService.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(".") ? loggerName.substring(0, loggerName.lastIndexOf(".") : ROOT;
return loggingSystem.getLoggerConfiguration(parentLoggerName).getEffectiveLevel().name();
}
/**
* 獲取當(dāng)前類(lèi)的Logger對(duì)象有效日志級(jí)別對(duì)應(yīng)的方法,進(jìn)行日志輸出。舉例:
* 如果當(dāng)前類(lèi)的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);
}
}
}
大家可以看到,Apollo的方式最終也是LoggingSystem 這個(gè)類(lèi)進(jìn)行修改日志級(jí)別的操作。
那可能大家會(huì)問(wèn),Apollo在這里的作用是什么?
如果大家用過(guò)Apollo的話就會(huì)發(fā)現(xiàn),在Apollo可視化管理系統(tǒng)中,每個(gè)系統(tǒng)都有一個(gè)實(shí)例列表,里面就是我們具體的應(yīng)用地址。所以在這里你可以認(rèn)為Apollo有類(lèi)似注冊(cè)中心的作用,在我們應(yīng)用啟動(dòng)的時(shí)候,Apollo后臺(tái)就會(huì)記錄下來(lái)。
所以Apollo能實(shí)現(xiàn)集群的日志級(jí)別動(dòng)態(tài)修改的原理就在這。是不是也很簡(jiǎn)單呢?
使用 MQ + LoggingSystem
如果你們的系統(tǒng)沒(méi)有接入Apollo的話,那應(yīng)該如何實(shí)現(xiàn)集群的日志級(jí)別動(dòng)態(tài)修改呢?
MQ就是其中一個(gè)選擇。我簡(jiǎn)單說(shuō)一下實(shí)現(xiàn)思路吧,具體實(shí)現(xiàn)也很簡(jiǎn)單,就留給大家去動(dòng)手實(shí)踐啦。
- 暴露一個(gè)修改日志級(jí)別的接口
- 這個(gè)接口要做的是使用producer來(lái)發(fā)送一個(gè)廣播類(lèi)型的MQ,注意了,是廣播類(lèi)型的
- 在consumer里面通過(guò)LoggingSystem進(jìn)行日志級(jí)別的修改即可。
是不是很簡(jiǎn)單呢?
使用Springboot的 actuator 組件
其實(shí)這種方法和方式一是差不多的,只是actuator把接口通過(guò)端點(diǎn)Endpoints 的方式暴露出來(lái)。
至于什么是端點(diǎn)(Endpoints),我簡(jiǎn)單介紹一下吧。
- 什么是端點(diǎn)
Endpoints 是 Actuator 的核心部分,它用來(lái)監(jiān)視應(yīng)用程序及交互,spring-boot-actuator中已經(jīng)內(nèi)置了非常多的Endpoints(health、info、beans、httptrace、shutdown等等),同時(shí)也允許我們擴(kuò)展自己的端點(diǎn)。
- 端點(diǎn)的分類(lèi)
Endpoints 分成兩類(lèi):原生端點(diǎn)和用戶自定義端點(diǎn);自定義端點(diǎn)主要是指擴(kuò)展性,用戶可以根據(jù)自己的實(shí)際應(yīng)用,定義一些比較關(guān)心的指標(biāo),在運(yùn)行期進(jìn)行監(jiān)控。
原生端點(diǎn)是在應(yīng)用程序里提供的眾多 restful api 接口,通過(guò)它們可以監(jiān)控應(yīng)用程序運(yùn)行時(shí)的內(nèi)部狀況。
原生端點(diǎn)又可以分成三類(lèi):
- 應(yīng)用配置類(lèi):可以查看應(yīng)用在運(yùn)行期間的靜態(tài)信息:例如自動(dòng)配置信息、加載的spring bean信息、yml文件配置信息、環(huán)境信息、請(qǐng)求映射信息;
- 度量指標(biāo)類(lèi):主要是運(yùn)行期間的動(dòng)態(tài)信息,例如堆棧、請(qǐng)求鏈、一些健康指標(biāo)、metrics信息等
- 操作控制類(lèi):主要是指shutdown,用戶可以發(fā)送一個(gè)請(qǐng)求將應(yīng)用的監(jiān)控功能關(guān)閉。
我們這里修改配置文件用到的就是應(yīng)用配置類(lèi)的端點(diǎn)。
查看當(dāng)前應(yīng)用各包/類(lèi)的日志級(jí)別
http://localhost:8080/actuator/loggers
可看到類(lèi)似如下的結(jié)果:
{
"levels": ["OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"],
"loggers": {
"ROOT": {
"configuredLevel": "INFO",
"effectiveLevel": "INFO"
},
"com.itmuch.logging.TestController": {
"configuredLevel": null,
"effectiveLevel": "INFO"
}
}
// ...省略
}
查看指定包/類(lèi)日志詳情
http://localhost:8080/actuator/loggers/com.dylan.logging.TestController
可看到類(lèi)似如下的結(jié)果:
{"configuredLevel":null,"effectiveLevel":"INFO"}
修改日志級(jí)別
POST方式,json格式的參數(shù)
example:http://localhost:8080/actuator/loggers/com.dylan.controller.IncreaseAgentController
actuator修改日志級(jí)別
但這種方式和方式一有同樣的局限性,就是只適合單機(jī)或者開(kāi)發(fā)環(huán)境。如果想用這種方式的話可以接入Spring Boot Admin。通過(guò)后臺(tái)的方式進(jìn)行管理。






