SpringBoot + MDC 實(shí)現(xiàn)全鏈路調(diào)用日志跟蹤

寫在前面
通過本文將了解到什么是 MDC市殷、MDC 應(yīng)用中存在的問題愕撰、如何解決存在的問題。
MDC 介紹
簡介
MDC(Mapped Diagnostic Context醋寝,映射調(diào)試上下文)是 log4j 搞挣、logback及l(fā)og4j2 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個與當(dāng)前線程綁定的哈希表音羞,可以往其中添加鍵值對囱桨。MDC 中包含的內(nèi)容可以被同一線程中執(zhí)行的代碼所訪問。當(dāng)前線程的子線程會繼承其父線程中的 MDC 的內(nèi)容嗅绰。當(dāng)需要記錄日志時舍肠,只需要從 MDC 中獲取所需的信息即可。MDC 的內(nèi)容則由程序在適當(dāng)?shù)臅r候保存進(jìn)去窘面。對于一個 Web 應(yīng)用來說翠语,通常是在請求被處理的最開始保存這些數(shù)據(jù)。
API 說明

clear() => 移除所有 MDC

get (String key) => 獲取當(dāng)前線程 MDC 中指定 key 的值

getContext() => 獲取當(dāng)前線程 MDC 的 MDC

put(String key, Object o) => 往當(dāng)前線程的 MDC 中存入指定的鍵值對

remove(String key) => 刪除當(dāng)前線程 MDC 中指定的鍵值對

優(yōu)點(diǎn)
代碼簡潔财边,日志風(fēng)格統(tǒng)一肌括,不需要在 log 打印中手動拼寫 traceId,即
LOGGER.info("traceId:{} ", traceId)
復(fù)制代碼
MDC 使用
添加攔截器
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果有上層調(diào)用就用上層的ID
String traceId = request.getHeader(Constants.TRACE_ID);
if (traceId == null) {
traceId = TraceIdUtil.getTraceId();
}

        MDC.put(Constants.TRACE_ID, traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        //調(diào)用結(jié)束后刪除
        MDC.remove(Constants.TRACE_ID);
    }
}

復(fù)制代碼
修改日志格式
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>
復(fù)制代碼
重點(diǎn)是 %X{traceId}酣难,traceId 和 MDC 中的鍵名稱一致谍夭。
簡單使用就這么容易畔况,但是在有些情況下 traceId 將獲取不到。
MDC 存在的問題

子線程中打印日志丟失 traceId

HTTP 調(diào)用丟失 traceId

......丟失traceId的情況慧库,來一個再解決一個,絕不提前優(yōu)化
解決 MDC 存在的問題
子線程日志打印丟失 traceId
子線程在打印日志的過程中 traceId 將丟失馋嗜,解決方式為重寫線程池齐板。對于直接 new 創(chuàng)建線程的情況不考略,實(shí)際應(yīng)用中應(yīng)該避免這種用法葛菇。重寫線程池?zé)o非是對任務(wù)進(jìn)行一次封裝甘磨。
線程池封裝類:ThreadPoolExecutorMdcWrapper.java
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}

public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}

public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}

public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}

@Override
public void execute(Runnable task) {
    super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}

@Override
public <T> Future<T> submit(Runnable task, T result) {
    return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
}

@Override
public <T> Future<T> submit(Callable<T> task) {
    return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}

@Override
public Future<?> submit(Runnable task) {
    return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}

}
復(fù)制代碼
說明:

繼承 ThreadPoolExecutor 類,重新執(zhí)行任務(wù)的方法眯停;

通過 ThreadMdcUtil 對任務(wù)進(jìn)行一次包裝

線程 traceId 封裝工具類:ThreadMdcUtil.java
public class ThreadMdcUtil {
public static void setTraceIdIfAbsent() {
if (MDC.get(Constants.TRACE_ID) == null) {
MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
}
}

public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
    return () -> {
        if (context == null) {
            MDC.clear();
        } else {
            MDC.setContextMap(context);
        }
        setTraceIdIfAbsent();
        try {
            return callable.call();
        } finally {
            MDC.clear();
        }
    };
}

public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
    return () -> {
        if (context == null) {
            MDC.clear();
        } else {
            MDC.setContextMap(context);
        }
        setTraceIdIfAbsent();
        try {
            runnable.run();
        } finally {
            MDC.clear();
        }
    };
}

}
復(fù)制代碼
說明(以封裝Runnable為例):

判斷當(dāng)前線程對應(yīng) MDC 的 Map 是否存在济舆,存在則設(shè)置;

設(shè)置 MDC 中的 traceId 值莺债,不存在則新生成滋觉,針對不是子線程的情況,如果是子線程齐邦,MDC 中 traceId 不為 null椎侠;

執(zhí)行 run 方法。

代碼等同于以下寫法措拇,會更直觀我纪。
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return new Runnable() {
@Override
public void run() {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
}
};
}
復(fù)制代碼
重新返回的是包裝后的 Runnable,在該任務(wù)執(zhí)行之前 runnable.run() 先將主線程的 Map 設(shè)置到當(dāng)前線程中(即 MDC.setContextMap(context))丐吓,這樣子線程和主線程 MDC 對應(yīng)的 Map 就是一樣的了浅悉。

判斷當(dāng)前線程對應(yīng) MDC 的 Map 是否存在,存在則設(shè)置券犁;

設(shè)置 MDC 中的 traceId 值樟遣,不存在則新生成。針對不是子線程的情況磁玉,如果是子線程妓羊,MDC 中 traceId 不為 null;

執(zhí)行 run 方法色难。

HTTP 調(diào)用丟失 traceId
在使用 HTTP 調(diào)用第三方服務(wù)接口時 traceId 將丟失泼舱,需要對 HTTP 調(diào)用工具進(jìn)行改造。發(fā)送時枷莉,在 request header 中添加 traceId娇昙,在下層被調(diào)用方添加攔截器獲取 header 中的 traceId 添加到 MDC 中。
HTTP 調(diào)用有多種方式笤妙,比較常見的有 HttpClient冒掌、OKHttp噪裕、RestTemplate,所以只給出這幾種 HTTP 調(diào)用的解決方式股毫。
HttpClient
實(shí)現(xiàn) HttpClient 攔截器
public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
String traceId = MDC.get(Constants.TRACE_ID);
//當(dāng)前線程調(diào)用中有traceId膳音,則將該traceId進(jìn)行透傳
if (traceId != null) {
//添加請求體
httpRequest.addHeader(Constants.TRACE_ID, traceId);
}
}
}
復(fù)制代碼
實(shí)現(xiàn) HttpRequestInterceptor 接口并重寫 process 方法。
如果調(diào)用線程中含有 traceId铃诬,則需要將獲取到的 traceId 通過 request 中的 header 向下透傳下去祭陷。
為 HttpClient 添加攔截器
private static CloseableHttpClient httpClient = HttpClientBuilder.create()
.addInterceptorFirst(new HttpClientTraceIdInterceptor())
.build();
復(fù)制代碼
通過 addInterceptorFirst 方法為 HttpClient 添加攔截器。
OKHttp
實(shí)現(xiàn) OKHttp 攔截器
public class OkHttpTraceIdInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
String traceId = MDC.get(Constants.TRACE_ID);
Request request = null;
if (traceId != null) {
//添加請求體
request = chain.request().newBuilder().addHeader(Constants.TRACE_ID, traceId).build();
}
Response originResponse = chain.proceed(request);

    return originResponse;
}

}
復(fù)制代碼
實(shí)現(xiàn) Interceptor 攔截器趣席,重寫 interceptor 方法兵志。實(shí)現(xiàn)邏輯和 HttpClient 差不多,如果能夠獲取到當(dāng)前線程的 traceId 則向下透傳宣肚。
為 OkHttp 添加攔截器
private static OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new OkHttpTraceIdInterceptor())
.build();
復(fù)制代碼
調(diào)用 addNetworkInterceptor 方法添加攔截器想罕。
RestTemplate
實(shí)現(xiàn) RestTemplate 攔截器
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
String traceId = MDC.get(Constants.TRACE_ID);
if (traceId != null) {
httpRequest.getHeaders().add(Constants.TRACE_ID, traceId);
}

    return clientHttpRequestExecution.execute(httpRequest, bytes);
}

}
復(fù)制代碼
實(shí)現(xiàn) ClientHttpRequestInterceptor 接口,并重寫 intercept 方法霉涨,其余邏輯都是一樣的按价,這里不做重復(fù)說明。
為 RestTemplate 添加攔截器
restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));
復(fù)制代碼
調(diào)用 setInterceptors 方法添加攔截器笙瑟。
第三方服務(wù)攔截器
HTTP 調(diào)用第三方服務(wù)接口全流程 traceId 需要第三方服務(wù)配合俘枫,第三方服務(wù)需要添加攔截器拿到 request header 中的 traceId 并添加到 MDC 中。
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果有上層調(diào)用就用上層的ID
String traceId = request.getHeader(Constants.TRACE_ID);
if (traceId == null) {
traceId = TraceIdUtils.getTraceId();
}

    MDC.put("traceId", traceId);
    return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
        throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
        throws Exception {
    MDC.remove(Constants.TRACE_ID);
}

}
復(fù)制代碼
說明:

先從 request header 中獲取t raceId逮走;

從 request header 中獲取不到 traceId 則說明不是第三方調(diào)用鸠蚪,直接生成一個新的 traceId;

將生成的 traceId 存入 MDC 中师溅。

除了需要添加攔截器之外茅信,還需要在日志格式中添加 traceId 的打印,如下:
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</prope

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末墓臭,一起剝皮案震驚了整個濱河市蘸鲸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌窿锉,老刑警劉巖酌摇,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嗡载,居然都是意外死亡窑多,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門洼滚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埂息,“玉大人,你說我怎么就攤上這事∏Э担” “怎么了享幽?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拾弃。 經(jīng)常有香客問我值桩,道長,這世上最難降的妖魔是什么豪椿? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任奔坟,我火速辦了婚禮,結(jié)果婚禮上砂碉,老公的妹妹穿的比我還像新娘。我一直安慰自己刻两,他們只是感情好增蹭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著磅摹,像睡著了一般滋迈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上户誓,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天饼灿,我揣著相機(jī)與錄音,去河邊找鬼帝美。 笑死碍彭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悼潭。 我是一名探鬼主播庇忌,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舰褪!你這毒婦竟也來了皆疹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤占拍,失蹤者是張志新(化名)和其女友劉穎略就,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晃酒,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡表牢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了贝次。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片初茶。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出恼布,到底是詐尸還是另有隱情螺戳,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布折汞,位于F島的核電站倔幼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏爽待。R本人自食惡果不足惜损同,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸟款。 院中可真熱鬧膏燃,春花似錦、人聲如沸何什。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽处渣。三九已至伶贰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間罐栈,已是汗流浹背黍衙。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荠诬,地道東北人琅翻。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像柑贞,于是被迫代替她去往敵國和親望迎。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容