背景
我們專案中現有日誌系統,採用的是slf4j+logback這套日誌元件,也是Java生態裡面比較常用的一個日誌元件,但是隨著分散式的演進,這套元件明視訊記憶體在以下幾個問題:
解決方案
正文
本篇部落格主題是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,然後提供一個介面查詢日誌,目前也有很多類似的開源框架整合了分散式鏈路日誌列印+看板