1. 程式人生 > >springboot+logback日誌輸出企業實踐(下)

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 日誌輸出
  • 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中的設定的內容。因此,在分散式應用在追蹤請求時,實現思路如下:

  1. web應用中,新增攔截器,在請求進入時,新增唯一id作為request-id,以標識此次請求。
  2. 新增此 request-id 到MDC中
  3. 若需要呼叫其它服務,把此request-id作為 header 引數
  4. 在日誌輸出時,新增此request-id的輸出作為標識
  5. 請求結束後,清除此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) {...// 後面給出}
}

此攔截器主要覆蓋 preHandleafterCompletion 方法,分別請求前和請求完成後的處理。使用 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技術記錄),獲取更多技術記錄: