背景
我們項目中現有日志系統,采用的是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,然后提供一個界面查詢日志,目前也有很多類似的開源框架集成了分布式鏈路日志打印+看板,例如 Cat、Zipkin、Pinpoint、SkyWalking
作者:DDZ_YYDS 出處:https://www.cnblogs.com/zdd-java/ 本文版權歸作者和博客園共有,歡迎轉載!但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接!