背景
我們項目中現有日誌系統,採用的是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 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,然後提供一個界面查詢日誌,目前也有很多類似的開源框架集成了分佈式鏈路日誌打印+看板