【原】MDC日誌鏈路設計

DDZ_YYDS發表於2021-12-01

背景

  我們專案中現有日誌系統,採用的是slf4j+logback這套日誌元件,也是Java生態裡面比較常用的一個日誌元件,但是隨著分散式的演進,這套元件明視訊記憶體在以下幾個問題:

  1.各種無關日誌穿行其中,導致我們可能無法直接定位整個操作流程。因此,我們可能需要對一個使用者的操作流程進行歸類標記,既在其日誌資訊上新增一個唯一標識,比如使用執行緒+時間戳,或者使用者身份標識等;從大量日誌資訊中grep出某個使用者的操作流程。
  2.無法做資訊埋點,也就不方便做後續系統、業務上進行分析
  3.日誌排查不方便,需要通過linux命令去匯出或者線上檢視日誌

解決方案

   筆者之前在攜程集團的時候,內部已經孵化了大量的中介軟體,其中分散式日誌元件已經應用在各大事業部下的不同應用,據統計整個集團上萬個應用都接入到這個日誌元件,根據印象大概畫了一個設計圖

 

 正文

  本篇部落格主題是MDC(MDC 全稱是 Mapped Diagnostic Context,可以粗略的理解成是一個執行緒安全的存放診斷日誌的容器),其具體流程是通過某些標識將整個軌跡串起來,例如A-B-C-遠端介面-D這條鏈路相關日誌資訊在日誌檔案裡可以通過某個標識快速查詢。下面介紹下目前我負責的專案中日誌方案

logback.xml

  將traceId配置在logback.xml,有點像佔位符的方式

 

 MDC

      將對應的traceId變數通過MDC寫入

 

原始碼分析

   1.MDC是什麼?

    下圖可知MDC是slf4j-api的一個類,裡面提供了put,get,remove等方法,看完原始碼其實可知就是一個ThreadLocal,每put一個元素就放到裡面,當呼叫logger.info的時候將ThreadLocal變數取出賦到輸出日誌

 

    

 

 

 

由上可知

1 MDCAdapter 是一個適配介面,存放於spi包下面,由此便知MDCAdapter是為了適配其它日誌元件

2 MDC 提供的 put 方法,可以將一個 K-V 的鍵值對放到容器中,並且能保證同一個執行緒內,Key 是唯一的,不同的執行緒 MDC 的值互不影響

3 在 logback.xml 中,在 layout 中可以通過宣告 %X{REQ_ID} 來輸出 MDC 中 REQ_ID 的資訊

4 MDC 提供的 remove 方法,可以清除 MDC 中指定 key 對應的鍵值對資訊

 

LogbackMDCAdapters原始碼

 

 


  如上是MDC的使用方法以及原始碼分析,下面介紹的是本地呼叫外部系統的時候,假設用 的是restTemplate,那麼得考慮如何把呼叫前後的日誌情況進行抽取封裝,做到統一列印,因為筆者之前的程式碼是沒有做抽取,導致每個不同的呼叫方法都要手動去寫log.info,這樣的做法雖然沒有大問題,但是明顯是比較多餘且可以進行抽取

 

外部介面日誌軌跡輸出

    呼叫過程中涉及到外部介面,由於外部介面是在第三方系統,我們無法將traceId傳遞下去,需要改造我們這邊的遠端呼叫程式碼,由於筆者專案用的是restTemplate,所以需要對restTemplate新增攔截器,用於傳送請求前和請求後列印出相關日誌,如下是我這邊的restTemplate對應的日誌攔截器

class MyRequestInterceptor implements ClientHttpRequestInterceptor {

        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
            traceRequest(request, bytes);

            ClientHttpResponse response = execution.execute(request, bytes);
            ClientHttpResponse responseCopy = new BufferingClientHttpResponseWrapper(response);
            traceResponse(responseCopy);
            return responseCopy;
        }

        /**
         * 列印請求資料
         *
         * @param request 請求
         * @param bytes   請求體
         */
        private void traceRequest(HttpRequest request, byte[] bytes) {
            String body = new String(bytes, StandardCharsets.UTF_8);
            log.info("Request Body = {}", body);
        }

        /**
         * 列印響應結果
         *
         * @param response 響應結果
         * @throws IOException io
         */
        private void traceResponse(ClientHttpResponse response) throws IOException {
            StringBuilder inputStringBuilder = new StringBuilder();
            try (BufferedReader bufferedReader =
                         new BufferedReader(new InputStreamReader(response.getBody(), StandardCharsets.UTF_8))) {
                String line = bufferedReader.readLine();
                while (line != null) {
                    inputStringBuilder.append(line);
                    // inputStringBuilder.append('\n');
                    line = bufferedReader.readLine();
                }
            }
            log.info("Response Body: {}", inputStringBuilder.toString());
        }

        final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
            private final ClientHttpResponse response;
            private byte[] body;

            BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
                this.response = response;
            }


            @Override
            public HttpStatus getStatusCode() throws IOException {
                return this.response.getStatusCode();
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return this.response.getRawStatusCode();
            }

            @Override
            public String getStatusText() throws IOException {
                return this.response.getStatusText();
            }

            @Override
            public HttpHeaders getHeaders() {
                return this.response.getHeaders();
            }

            @Override
            public InputStream getBody() throws IOException {
                if (this.body == null) {
                    this.body = StreamUtils.copyToByteArray(this.response.getBody());
                }
                return new ByteArrayInputStream(this.body);
            }

            @Override
            public void close() {
                this.response.close();
            }

        }
    }

  

最後

   以上就是關於MDC常見的使用場景,包括攜程裡面的日誌元件其實內部也是通過MDC實現,只不過是根據業務做了調整,一般分散式環境下最好將日誌輸出到Redis或者ES,然後提供一個介面查詢日誌,目前也有很多類似的開源框架整合了分散式鏈路日誌列印+看板

 

 

相關文章