世界上最快的捷徑,就是腳踏實地,本文已收錄架構技術專欄關注這個喜歡分享的地方。
開源項目:
- 分布式監控(Gitee GVP最有價值開源項目 ):https://gitee.com/sanjiankethree/cubic
- 攝像頭視頻流采集:https://gitee.com/sanjiankethree/cubic-video
一、簡介
Spring Boot 版本: 2.3.4.RELEASE
不知道大家有沒有過當線上出現問題的時候,需要某些DEBUG日志,但奈何當前使用時INFO。
如果想啟用DEBUG就需要重新打包發版,但某些場景下重啟有可能問題就不會復現了,真是腦闊疼啊。
今天我們就來說下Spring Boot 下的日志配置動態調整,讓你的日志級別隨心而動。
Spring Boot的日志
在Spring Boot 內部使用的其實是Commons Logging, 而基於Spring Boot的配置加載機制為我們提供了Java Util Logging、Log4j2、Logback幾種日志方式。
Logback是其默認的日志框架,如果沒有特殊的必要真不建議更換了。(不要說性能了)
日志格式
不要小瞧格式這種東西,在實際應用的時候是賊拉重要的一件事。
不知道大家的公司有沒有統一的日志基礎組件,當然沒有也大概會有統一的日志配置文件吧。
想想如果你的日志格式不統一的話,如果每個項目都有自己的風格的話,你叫你的運維小伙伴怎么幫你切分日志?幫你報警呢?那真是正則寫到死,完全靠愛發電了。(比如我們使用的Loghub 不統一要被運維打死的)
來看下我們的日志格式配置,這里只放PATTERN
-|%d{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%X{tid}|%thread|%logger{36}.%M:%L-%msg%n
先解釋下各個位置:
%d{yyyy-MM-dd HH:mm:ss.SSS}
:時間%-5level
: 日志級別%X{tid}
: 我們自定義的分布式追蹤ID%thread
: 線程%logger{36}.%M:%L
:class的全名(36代表最長字符).信息 行號%msg%n
: 輸出信息 換行
這里不知道大家能否理解前面有一個 -|
是為了什么? 其實是為了在正則切分的時候方便區分換行的日志,如異常堆棧的信息。
幾個知識點
再說幾個其他Spring Boot使用的小點,我們就來進入正題
-
在這里Logback 沒有FATAL 級別,被歸到ERROR里面了
-
可以在
application.properties
里面配置debug=true
來開啟debug模式,你也可以配置trace=true
開啟 trace模式 -
可以再
application.properties
里使用logging.level.<logger-name>=<level>
這種格式來配置各種日志級別,比如org.hibernate
級別被設置為ERRORlogging.level.org.hibernate=error
-
日志組的概念,如果這么一個個配置煩死了,可以設定一個組給它整體配置。如:
logging.group.tomcat=org.apache.catalina, org.apache.coyote
配置級別為TRACElogging.level.tomcat=TRACE
-
如果使用Logback配置文件,官方建議使用
logback-spring.xml
這樣的名稱,如果使用logback.xml
這樣的,Spring的日志初始化就該有問題了。 -
如果你控制日志配置,但又不想用logback.xml作為Logback配置的名字,可以在application.properties配置文件里面通過logging.config屬性指定自定義的名字:
logging.config=classpath:xxx-log.xml
二、動態修改日志級別
下面我們就來說說在運行狀態下的Spring Boot應用是怎么進行動態日志級別變更的
Spring Boot Actuator
Actuator 想必了解過Spring Boot的都知道它的大名,監控、審計、度量信息等等。我們使用的優雅停機、健康檢查、日志信息都是通過這玩意來實現的。
在我們使用了Actuator 后,我們就可以使用其Logger的REST接口來操作我們的日志了,有如下三個
GET http://127.0.0.1:6080/actuator/loggers
返回當前應用全部的日志級別信息(想要啥有啥)GET http://127.0.0.1:6080/actuator/loggers/{name}
返回{name}的日志級別POST http://127.0.0.1:6080/actuator/loggers/{name}
配置參數{"configuredLevel":"INFO","effectiveLevel":"INFO"}
修改日志級別
使用Actuator 機制動態修改級別
1)、依賴必要的配置 Spring Boot Actuator (如果你繼承了parent是不需要version的)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
2)、配置文件
#注意,這里我只開啟了loggers,health兩個Actuator
management.endpoints.web.exposure.include=loggers,health
management.endpoint.loggers.enabled=true
3)、創建一個Controller
@RestController
@RequestMapping("/log")
public class TestLogController {
private Logger log = LoggerFactory.getLogger(TestLogController.class);
@GetMapping
public String log() {
log.trace("This is a TRACE level message");
log.debug("This is a DEBUG level message");
log.info("This is an INFO level message");
log.warn("This is a WARN level message");
log.error("This is an ERROR level message");
return "See the log for details";
}
}
4)、啟動應用
此時在你啟動應用后,如果你使用IDEA,就可以看到如下Mapping,有三個關於logger的REST接口
5)、測試
此時,我先執行GET http://127.0.0.1:6080/log
,控制台打印如下信息
2020-10-15 23:14:51.204 INFO 52628 --- [nio-6080-exec-7] c.q.chapter1.failure.TestLogController : This is an INFO level message
2020-10-15 23:14:51.220 WARN 52628 --- [nio-6080-exec-7] c.q.chapter1.failure.TestLogController : This is a WARN level message
2020-10-15 23:14:51.221 ERROR 52628 --- [nio-6080-exec-7] c.q.chapter1.failure.TestLogController : This is an ERROR level message
此時我們執行GET http://127.0.0.1:6080/actuator/loggers/ROOT
返回{"configuredLevel":"INFO","effectiveLevel":"INFO"}
,代表此時我們應用的日志級別是INFO,ROOT表示的是根節點。
6)、修改級別
此時我們不想再看INFO信息,想將整個應用日志級別換位WARN的話,我們來執行POST http://127.0.0.1:6080/actuator/loggers/ROOT
參數為{"configuredLevel":"TRACE","effectiveLevel":"TRACE"}
此時我們再來執行GET http://127.0.0.1:6080/log
,控制台打印如下信息
2020-10-15 23:24:11.481 TRACE 53552 --- [nio-6080-exec-3] c.q.chapter1.failure.TestLogController : This is a TRACE level message
2020-10-15 23:24:11.481 DEBUG 53552 --- [nio-6080-exec-3] c.q.chapter1.failure.TestLogController : This is a DEBUG level message
2020-10-15 23:24:11.481 INFO 53552 --- [nio-6080-exec-3] c.q.chapter1.failure.TestLogController : This is an INFO level message
2020-10-15 23:24:11.481 WARN 53552 --- [nio-6080-exec-3] c.q.chapter1.failure.TestLogController : This is a WARN level message
2020-10-15 23:24:11.481 ERROR 53552 --- [nio-6080-exec-3] c.q.chapter1.failure.TestLogController : This is an ERROR level message
另幾種方法
-
配置文件掃描: 就是Logback 自動掃描的特性修改級別,配合
logback-spring.xml
開啟<configuration scan="true" scanPeriod="15 seconds">
就可以實現動態修改logback-spring.xml
內部日志級別的目的了,有興趣可以自己試試。 -
arthas 動態修改
-
結合遠程配置中心,如Apollo實現級別動態修改
三、實現原理
這里我們主要使用的是Spring Boot Actuator Log
,所以我們也就來說說它的原理。
Endpoint的加載
首先我們從依賴spring-boot-actuator
中找到我們的LoggersEndpoint
(所有的Actuator都是這一個路數),如圖:
熟悉Spring Boot加載機制的朋友都了解,在每個actuator Endpoint的背后,必然還會存在一個xxxEndpointAutoConfiguration
來為我們進行Endpoint 的加載。
而這些加載機制就都存放在spring-boot-actuator-autoconfigure
中,我們在其中可以找到LoggersEndpointAutoConfiguration
用於加載LoggersEndpoint
的配置類。
來看下它的核心:
@Bean
@ConditionalOnBean(LoggingSystem.class)
@Conditional(OnEnabledLoggingSystemCondition.class)
@ConditionalOnMissingBean
public LoggersEndpoint loggersEndpoint(LoggingSystem loggingSystem,
ObjectProvider<LoggerGroups> springBootLoggerGroups) {
return new LoggersEndpoint(loggingSystem, springBootLoggerGroups.getIfAvailable(LoggerGroups::new));
}
可以看到兩個重要的參數:
- LoggingSystem:一個抽象頂級類
- springBootLoggerGroups:存儲了當前日志分組數據
總結下:
1、我們依賴了spring-boot-starter-actuator
包后,里面依賴了spring-boot-actuator-autoconfigure
2、在啟動掃描到spring-boot-actuator-autoconfigure
下的META-INF/spring.factories
時,LoggersEndpointAutoConfiguration
會被加載到
3、LoggersEndpointAutoConfiguration
內又聲明了LoggersEndpoin
並賦值LoggingSystem
和springBootLoggerGroups
作為其參數
4、項目啟動后我們通過LoggersEndpoint
接口進行日志數據訪問
LoggingSystem
LoggingSystem的繼承關系
通過上面可以了解到 LoggingSystem
就是日志操作管理的核心了,所以我們先來看下他的Diagrams
通過繼承關系一眼可以看到,LogbackLoggingSystem
就是我們的正主了,雖然說知道了正主那我們的LoggingSystem到底是怎么加載的呢?
LoggingSystem的加載
主要參與類說下:
LoggingApplicationListener
ApplicationStartingEvent
LogbackLoggingSystem
LoggingSystem
不多說,先上圖
1、應用使用SpringApplication.run(Chapter1Application.class, args);
啟動
2、發送啟動事件ApplicationStartingEvent
、ApplicationEnvironmentPreparedEvent
等
3、LoggingApplicationListener
接收事件進行事件分發
4、LoggingApplicationListener
接收事件ApplicationStartingEvent
5、LoggingApplicationListener
調用內部onApplicationStartingEvent(ApplicationStartingEvent event)
方法,使用LoggingSystem.get(classloader)初始化LoggingSystem。
6、然后調用LogbackLoggingSystem.beforeInitialize()
(因為這里我們用的是logback)
7、LoggingApplicationListener
接收事件ApplicationEnvironmentPreparedEvent
8、LoggingApplicationListener
調用內部onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event)
方法
9、onApplicationEnvironmentPreparedEvent
調用initialize(ConfigurableEnvironment environment, ClassLoader classLoader)
方法,會根據環境變量配置進行logging system的初始化
LoggersEndpoint
最后,我們就來看看LoggersEndpoint就可以了,這里為了方便解讀我就不全拷貝過來了,需要哪里選哪里
首先看我們對日志操作的三個接口:
-
GET /actuator/loggers
對應public Map<String, Object> loggers()
方法 -
GET /actuator/loggers/{name}
對應public LoggerLevels loggerLevels(@Selector String name)
方法 -
POST /actuator/loggers/{name}
對應public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel)
方法
這里拿一個public Map<String, Object> loggers()
方法舉例說明,其他的差不多
@ReadOperation
public LoggerLevels loggerLevels(@Selector String name) {
Assert.notNull(name, "Name must not be null");
LoggerGroup group = this.loggerGroups.get(name);
if (group != null) {
return new GroupLoggerLevels(group.getConfiguredLevel(), group.getMembers());
}
LoggerConfiguration configuration = this.loggingSystem.getLoggerConfiguration(name);
return (configuration != null) ? new SingleLoggerLevels(configuration) : null;
}
代碼其實很簡單,就不做過多解讀了。
四、絮叨
其實日志在我們的系統應用中很重要,對於問題的排查也是重要的憑證。根據經驗我們系統的日志最好能做到幾個點:
-
日志格式統一化
-
最好提供統一的日志組件,比如我們就使用了公共的logback-spring.xml組件。如果我們需要修改日志某些特性,比如加APM日志等等,只需要改一個點,我們系統的所以日志狀態都會發生改變。
-
最好使用動態配置,調整日志配置,比如Apollo
世界上最快的捷徑,就是腳踏實地,本文已收錄架構技術專欄關注這個喜歡分享的地方。
開源項目:
- 分布式監控(Gitee GVP最有價值開源項目 ):https://gitee.com/sanjiankethree/cubic
- 攝像頭視頻流采集:https://gitee.com/sanjiankethree/cubic-video