01、前言
在現網出現故障時,我們經常需要獲取一次請求流程里的所有日志進行定位。如果請求只在一個線程里處理,則我們可以通過線程ID來過濾日志,但如果請求包含異步線程的處理,那么光靠線程ID就顯得捉襟見肘了。
華為IoT平臺,提供了接收設備上報數據的能力, 當數據到達平臺后,平臺會進行一些復雜的業務邏輯處理,如數據存儲,規則引擎,數據推送,命令下發等等。由于這個邏輯之間沒有強耦合的關系,所以通常是異步處理。如何將一次數據上報請求中包含的所有業務日志快速過濾出來,就是本文要介紹的。
02、正文
SLF4J日志框架提供了一個MDC(Mapped Diagnostic Contexts)工具類,谷歌翻譯為映射的診斷上下文,從字面上很難理解,我們可以先實戰一把。
publicclassMain{ privatestaticfinalStringKEY="requestId"; privatestaticfinalLoggerlogger=LoggerFactory.getLogger(Main.class); publicstaticvoidmain(String[]args){ //入口傳入請求ID MDC.put(KEY,UUID.randomUUID().toString()); //打印日志 logger.debug("loginmainthread1"); logger.debug("loginmainthread2"); logger.debug("loginmainthread3"); //出口移除請求ID MDC.remove(KEY); } }
我們在main函數的入口調用MDC.put()方法傳入請求ID,在出口調用MDC.remove()方法移除請求ID。配置好log4j2.xml文件后,運行main函數,可以在控制臺看到以下日志輸出:
2018-02-1713:19:52.606{requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0}[main]DEBUGcn.wudashan.Main-loginmainthread1 2018-02-1713:19:52.609{requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0}[main]DEBUGcn.wudashan.Main-loginmainthread2 2018-02-1713:19:52.609{requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0}[main]DEBUGcn.wudashan.Main-loginmainthread3
從日志中可以明顯地看到花括號中包含了(映射的)*請求ID(requestId),這其實就是我們定位*(診斷)*問題的關鍵字*(上下文)。有了MDC工具,只要在接口或切面植入put()和remove()代碼,在現網定位問題時,我們就可以通過grep requestId=xxx *.log快速的過濾出某次請求的所有日志。
03、進階
然而,MDC工具真的有我們所想的這么方便嗎?回到我們開頭,一次請求可能涉及多線程異步處理,那么在多線程異步的場景下,它是否還能正常運作呢?Talk is cheap, show me the code。
publicclassMain{ privatestaticfinalStringKEY="requestId"; privatestaticfinalLoggerlogger=LoggerFactory.getLogger(Main.class); publicstaticvoidmain(String[]args){ //入口傳入請求ID MDC.put(KEY,UUID.randomUUID().toString()); //主線程打印日志 logger.debug("loginmainthread"); //異步線程打印日志 newThread(newRunnable(){ @Override publicvoidrun(){ logger.debug("loginotherthread"); } }).start(); //出口移除請求ID MDC.remove(KEY); } }
代碼里我們新起了一個異步線程,并在匿名對象Runnable的run()方法打印日志。運行main函數,可以在控制臺看到以下日志輸出:
2018-02-1714:05:43.487{requestId=e6099c85-72be-4986-8a28-de6bb2e52b01}[main]DEBUGcn.wudashan.Main-loginmainthread 2018-02-1714:05:43.490{}[Thread-1]DEBUGcn.wudashan.Main-loginotherthread
不幸的是,請求ID在異步線程里不打印了。這是怎么回事呢?要解決這個問題,我們就得知道MDC的實現原理。由于篇幅有限,這里就暫不詳細介紹,MDC之所以在異步線程中不生效是因為底層采用ThreadLocal作為數據結構,我們調用MDC.put()方法傳入的請求ID只在當前線程有效。感興趣的小伙伴可以自己深入一下代碼細節。
知道了原理那么解決這個問題就輕而易舉了,我們可以使用裝飾器模式,新寫一個MDCRunnable類對Runnable接口進行一層裝飾。在創建MDCRunnable類時保存當前線程的MDC值,在執行run()方法時再將保存的MDC值拷貝到異步線程中去。代碼實現如下:
publicclassMDCRunnableimplementsRunnable{ privatefinalRunnablerunnable; privatefinalMapmap; publicMDCRunnable(Runnablerunnable){ this.runnable=runnable; //保存當前線程的MDC值 this.map=MDC.getCopyOfContextMap(); } @Override publicvoidrun(){ //傳入已保存的MDC值 for(Map.Entry entry:map.entrySet()){ MDC.put(entry.getKey(),entry.getValue()); } //裝飾器模式,執行run方法 runnable.run(); //移除已保存的MDC值 for(Map.Entry entry:map.entrySet()){ MDC.remove(entry.getKey()); } } }
接著,我們需要對main函數里創建的Runnable實現類進行裝飾:
publicclassMain{ privatestaticfinalStringKEY="requestId"; privatestaticfinalLoggerlogger=LoggerFactory.getLogger(Main.class); privatestaticfinalExecutorServiceEXECUTOR=Executors.newSingleThreadExecutor(); publicstaticvoidmain(String[]args){ //入口傳入請求ID MDC.put(KEY,UUID.randomUUID().toString()); //主線程打印日志 logger.debug("loginmainthread"); //異步線程打印日志,用MDCRunnable裝飾Runnable newThread(newMDCRunnable(newRunnable(){ @Override publicvoidrun(){ logger.debug("loginotherthread"); } })).start(); //異步線程池打印日志,用MDCRunnable裝飾Runnable EXECUTOR.execute(newMDCRunnable(newRunnable(){ @Override publicvoidrun(){ logger.debug("loginotherthreadpool"); } })); EXECUTOR.shutdown(); //出口移除請求ID MDC.remove(KEY); } }
執行main函數,將會輸出以下日志:
2018-03-042305.343{requestId=5ee2a117-e090-41d8-977b-cef5dea09d34}[main]DEBUGcn.wudashan.Main-loginmainthread 2018-03-042305.346{requestId=5ee2a117-e090-41d8-977b-cef5dea09d34}[Thread-1]DEBUGcn.wudashan.Main-loginotherthread 2018-03-042305.347{requestId=5ee2a117-e090-41d8-977b-cef5dea09d34}[pool-2-thread-1]DEBUGcn.wudashan.Main-loginotherthreadpool
Congratulations!經過我們的努力,最終在異步線程和線程池中都有requestId打印了!
04、總結
本文講述了如何使用MDC工具來快速過濾一次請求的所有日志,并通過裝飾器模式使得MDC工具在異步線程里也能生效。有了MDC,再通過AOP技術對所有的切面植入requestId,就可以將整個系統的任意流程的日志過濾出來。使用MDC工具,在開發自測階段,可以極大地節省定位問題的時間,提升開發效率;在運維維護階段,可以快速地收集相關日志信息,加快分析速度。
審核編輯:劉清
-
AOP
+關注
關注
0文章
40瀏覽量
11170
原文標題:Spring Boot 如何快速過濾出一次請求的所有日志?
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
啟動Spring Boot項目應用的三種方法
java 日志框架Spring Boot分析
Spring Boot從零入門1 詳述
Spring Boot特有的實踐
強大的Spring Boot 3.0要來了
SpringBoot如何快速過濾出一次請求的所有日志?
怎樣使用Kiuwan保護Spring Boot應用程序呢?
Spring Boot如何實現日志鏈路追蹤

Spring Boot的日志框架使用

Spring Boot Actuator快速入門
Spring Boot啟動 Eureka流程

Spring Boot的啟動原理

評論