springboot+logback日誌輸出企業實踐(下)

一句話概括: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 元素),添加後,輸出內容如下所示:

logback狀態數據

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日誌輸出

注意,示例代碼沒有給出調用其它服務時的處理,當調用時,從 MDC 中獲取 request-id ,然後把它作爲 header參數,實現 request-id 的傳遞。這樣查詢日誌時,根據此id來追蹤就可以了。

6. 總結

本篇文章針對springboot應用開發中,如何更好的使用 logback 解決日誌輸出的相關問題,主要包括 loback 狀態數據的輸出,使用異步解決日誌輸出性能問題,配置多環境下的日誌輸出以及使用MDC解決分佈式應用追蹤請求。希望能對大家有幫助。

本文中使用的示例代碼已放在我的githubhttps://github.com/mianshenglee/my-example/tree/master/springboot-logback-demo,有興趣的同學可以pull代碼,結合示例一起學習。

參考資料

往期文章

關注我的公衆號(搜索Mason技術記錄),獲取更多技術記錄:

mason

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章