【原】MDC日誌鏈路設計

背景

  我們項目中現有日誌系統,採用的是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 MDCRequestInterceptor 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實現,只不過是根據業務做了調整;本博客日誌只是在本地輸出到log文件,一般分佈式環境下最好將日誌輸出到Redis或者ES,然後提供一個界面查詢日誌,目前也有很多類似的開源框架集成了分佈式鏈路日誌打印+看板

 

 

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