springboot+logback日誌輸出企業實踐(下)
目錄
- 1.引言
- 2. 輸出 logback 狀態資料
- 3. logback 非同步輸出日誌
- 3.1 非同步輸出配置
- 3.2 非同步輸出原理
- 4. springboot 多環境下 logback 配置
- 5. MDC 分散式應用追蹤請求
- 5.1 新增攔截器
- 5.1.1 攔截器實現
- 5.1.2 註冊攔截器到web配置中
- 5.2 設定 MDC 日誌輸出
- 5.1 新增攔截器
- 6. 總結
- 參考資料
- 往期文章
一句話概括:logback 在實現了基本的日誌輸出到檔案功能後,在企業實踐中,還會有其它的進階需求,本文對logback的進階使用進行描述。
1.引言
上一篇文章《springboot+logback 日誌輸出企業實踐(上)》對 logback 的使用及配置進行描述,並實現按日誌級別輸出到獨立檔案功能。但在企業實踐中,還會有其它的需求,如需要在多環境下使用不同日誌級別,日誌輸出效能低怎麼處理,還有分散式系統如何追蹤請求日誌等等,對於這些需求,logback 有提供相應的功能,本文將對這幾種需求的實現進行講解。具體有如下內容:
- 使用非同步輸出日誌提高效能
- logback 在多環境下選擇日誌級別配置
- 使用 MDC 在分散式系統中追蹤請求
如需看原始碼,本文示例工程地址:https://github.com/mianshenglee/my-example/tree/master/springboot-logback-demo
2. 輸出 logback 狀態資料
logback 官方文件指出,強烈建議啟用 logback 狀態資料的輸出,將會在很大程度上幫助我們診斷 logback 相關問題。通過這些狀態資料,可以知道 logback 配置檔案載入情況,配置中對應的 appender,logger的裝載情況等。啟用狀態資料輸出有兩種方式:
- 在根元素( configuration ) 中設定屬性
debug="true"
- 新增元素( statusListener ),class 使用
OnConsoleStatusListener
。如下:
<!-- 輸出logback的本身狀態資料 -->
<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
注意,二者選其一即可,此處的 debug 與配置檔案中的日誌級別沒有關係,只用於表示輸出狀態資料。
本示例中,使用第二種方式(新增 statusListener
元素),新增後,輸出內容如下所示:
3. logback 非同步輸出日誌
3.1 非同步輸出配置
按之前的 logback 配置,日誌輸出到檔案是同步輸出的,即每次輸出都會直接寫IO到磁碟檔案,從而產生阻塞,造成不必要的效能損耗。當然,對於一般的應用,影響不大,但對於高併發的應用,還是有必要對效能進行優化的。logback 提供了日誌非同步輸出的 AsyncAppender。 非同步輸出日誌的方式很簡單,新增一個基於非同步寫日誌的appender
,並指向原先配置的appender
即可 。見以下配置:
<!-- 非同步輸出 -->
<appender name="ASYNCDEBUG" class="ch.qos.logback.classic.AsyncAppender">
<!-- 預設如果佇列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日誌,若要保留全部日誌,設定為0 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改預設的佇列的深度,該值會影響效能.預設值為256 -->
<queueSize>1024</queueSize>
<!-- 新增附加的appender,最多隻能新增一個 -->
<appender-ref ref="DEBUGFILE"/>
<includeCallerData>true</includeCallerData>
</appender>
//INFO 結構同上,略
//WARN 結構同上,略
//ERROR 結構同上,略
<!-- 非同步輸出關聯到root -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNCDEBUG" />
...//略
</root>
AsyncAppender 對應需要設定的引數主要有 :
屬性名 | 型別 | 描述 |
---|---|---|
queueSize | int |
佇列的最大容量,預設為 256 |
discardingThreshold | int |
預設,當佇列還剩餘 20% 的容量時,會丟棄級別為 TRACE, DEBUG 與 INFO 的日誌,僅僅只保留 WARN 與 ERROR 級別的日誌。想要保留所有的事件,可以設定為 0 |
includeCallerData | boolean |
獲取呼叫者的資料相對來說比較昂貴。為了提高效能,預設情況下不會獲取呼叫者的資訊。預設情況下,只有像執行緒名或者 MDC 這種"便宜"的資料會被複制。設定為 true 時,appender 會包含呼叫者的資訊 |
maxFlushTime | int |
根據所引用 appender 佇列的深度以及延遲, AsyncAppender 可能會耗費長時間去重新整理佇列。當 LoggerContext 被停止時, AsyncAppender stop 方法會等待工作執行緒指定的時間來完成。使用 maxFlushTime 來指定最大的重新整理時間,單位為毫秒。在指定時間內沒有被處理完的事件將會被丟棄。這個屬性的值的含義與 Thread.join(long) 相同 |
neverBlock | boolean |
預設為 false,在佇列滿的時候 appender 會阻塞而不是丟棄資訊。設定為 true,appender 不會阻塞你的應用而會將訊息丟棄 |
3.2 非同步輸出原理
AsyncAppender 的實現方式是通過阻塞佇列( BlockingQueue
)來避免日誌直接輸出到檔案,而是把日誌事件輸出到 BlockingQueue
中,然後啟動一個新的worker執行緒,主執行緒不阻塞,worker執行緒則從佇列中獲取需要寫的日誌,非同步輸出到對應的位置。
4. springboot 多環境下 logback 配置
使用 springboot 進行應用開發,支援對多環境的配置支援,只需要按application-*.properties
格式新增配置檔案,然後使用 spring.profiles.active
指定環境即可。同樣,日誌輸出,一般在開發環境,使用 DEBUG 級別,以便以檢查問題,而在生產環境,則只輸出 ERROR 級別的日誌。如下所示,profile定義開發環境為 dev ,生產環境為 prod:
<!-- 開發環境:debug級別-->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
...//略
</root>
</springProfile>
<!-- 生產環境:error級別-->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="STDOUT"/>
...//略
</root>
</springProfile>
上述配置是對 root 進行 設定(當然,其它元素也可以使用)。經過此設定後,則會根據 spring.profiles.active
而決定使用指定日誌級別輸出。
其實 logback 還支援使用 if 元素,使用 if-then-else 的形式,結合 condition 屬性來實現條件處理。有興趣的讀者可以看官方文件說明 "Conditional processing of configuration files"
5. MDC 分散式應用追蹤請求
使用springboot開發分散式應用,很多都微服務化,當請求過來,可能需要呼叫多個服務來完成請求動作。在查詢日誌時,特別是請求量大的情況下,日誌多,很難找到對應請求的日誌,造成定位異常難,日誌難以追蹤等問題。針對此類問題,logback 提供了 MDC ( Mapped Diagnostic Contexts 診斷上下文對映 ),MDC可以讓開發人員可以在 診斷上下文 中放置資訊,這些訊息是內部使用了 ThreadLocal實現了執行緒與執行緒之間的資料隔離,管理每個執行緒的上下文資訊 。而在日誌輸出時,可以通過識別符號%X{key}
來輸出MDC中的設定的內容。因此,在分散式應用在追蹤請求時,實現思路如下:
- web應用中,新增攔截器,在請求進入時,新增唯一id作為
request-id
,以標識此次請求。 - 新增此
request-id
到MDC中 - 若需要呼叫其它服務,把此
request-id
作為 header 引數 - 在日誌輸出時,新增此
request-id
的輸出作為標識 - 請求結束後,清除此
request-id
5.1 新增攔截器
5.1.1 攔截器實現
通過攔截器,實現在請求前新增request-id
,並放到 MDC 中;請求完成後清除的動作。新增包 interceptor
存放攔截器類,類定義如下:
@Slf4j
@Component
public class RequestIdTraceInterceptor implements HandlerInterceptor {
public static final String REQUEST_ID_KEY = "request-id";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
MDC.put(REQUEST_ID_KEY, getRequestId(request));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
//把requestId新增到響應頭,以便其它應用使用
response.addHeader(REQUEST_ID_KEY, MDC.get(REQUEST_ID_KEY));
//請求完成,從MDC中移除requestId
MDC.remove(REQUEST_ID_KEY);
}
public static String getRequestId(HttpServletRequest request) {...// 後面給出}
}
此攔截器主要覆蓋 preHandle
及 afterCompletion
方法,分別請求前和請求完成後的處理。使用 MDC.put()
及 MDC.remove()
實現對MDC的寫入及清除操作。
在獲取 request-id
時,使用方法是 getRequestId()
,如下所示:
public static String getRequestId(HttpServletRequest request) {
String requestId;
String parameterRequestId = request.getParameter(REQUEST_ID_KEY);
String headerRequestId = request.getHeader(REQUEST_ID_KEY);
// 根據請求引數或請求頭判斷是否有“request-id”,有則使用,無則建立
if (parameterRequestId == null && headerRequestId == null) {
log.debug("no request-id in request parameter or header");
requestId = IdUtil.simpleUUID();
} else {
requestId = parameterRequestId != null ? parameterRequestId : headerRequestId;
}
return requestId;
}
根據請求引數或請求頭判斷是否有“request-id”,有則使用,無則建立,建立的request-id
為simpleUUID,以此作為唯一標識。
5.1.2 註冊攔截器到web配置中
新增 config
包用於存放配置檔案。繼承 WebMvcConfigurer
實現 addInterceptors
來新增攔截器到 web 配置中:
@Configuration
public class WebAppConfig implements WebMvcConfigurer {
@Autowired
RequestIdTraceInterceptor requestIdTraceInterceptor;
/**
* 新增攔截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//新增requestId
registry.addInterceptor(requestIdTraceInterceptor);
}
}
5.2 設定 MDC 日誌輸出
logback 的 MDC 輸出是用%X{key}
來作識別符號進行輸出,因此,修改 logback-spring.xml
檔案,在輸出格式中新增 %X{request-id}
輸出,如下:
<property name="log.pattern"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%10thread] [%X{request-id}] %40.40logger{40} [%10method,%line] : %msg%n"/>
至此,MDC處理完畢,啟動應用,訪問其中的某一個介面,輸出如下(其中8e955ff61fa7494788f52891a4fdbc6a
即可 request-id
):
注意,示例程式碼沒有給出呼叫其它服務時的處理,當呼叫時,從 MDC 中獲取
request-id
,然後把它作為 header引數,實現request-id
的傳遞。這樣查詢日誌時,根據此id來追蹤就可以了。
6. 總結
本篇文章針對springboot應用開發中,如何更好的使用 logback 解決日誌輸出的相關問題,主要包括 loback 狀態資料的輸出,使用非同步解決日誌輸出效能問題,配置多環境下的日誌輸出以及使用MDC解決分散式應用追蹤請求。希望能對大家有幫助。
本文中使用的示例程式碼已放在我的github:https://github.com/mianshenglee/my-example/tree/master/springboot-logback-demo
,有興趣的同學可以pull程式碼,結合示例一起學習。
參考資料
logback官網:
http://logback.qos.ch
logback中文手冊:
http://www.logback.cn/
logback github:
https://github.com/qos-ch/logback
使用requestId在分散式系統追蹤請求:
https://juejin.im/post/5b51f85c5188251af91a7525
往期文章
- springboot+logback 日誌輸出企業實踐(上)
- springboot+swagger 介面文件企業實踐(下)
- springboot+swagger介面文件企業實踐(上)
- 查閱了十幾篇學習資源後,我總結了這份AI學習路徑
- java應用監測(8)-阿里診斷工具arthas
- java應用監測(7)-線上動態診斷神器BTrace
關注我的公眾號(搜尋Mason技術記錄
),獲取更多技術記錄: