0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

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

jf_ro2CN3Fa ? 來源:稀土掘金技術(shù)社區(qū) ? 2023-01-07 13:47 ? 次閱讀

寫在前面

通過本文將了解到什么是MDC、MDC應(yīng)用中存在的問題、如何解決存在的問題

基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

MDC介紹

簡(jiǎn)介:

MDC(Mapped Diagnostic Context,映射調(diào)試上下文)是 log4j 、logback及l(fā)og4j2 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個(gè)與當(dāng)前線程綁定的哈希表 ,可以往其中添加鍵值對(duì)。MDC 中包含的內(nèi)容可以被同一線程中執(zhí)行的代碼所訪問 。

當(dāng)前線程的子線程會(huì)繼承其父線程中的 MDC 的內(nèi)容。當(dāng)需要記錄日志時(shí),只需要從 MDC 中獲取所需的信息即可。MDC 的內(nèi)容則由程序在適當(dāng)?shù)臅r(shí)候保存進(jìn)去。對(duì)于一個(gè) Web 應(yīng)用來說,通常是在請(qǐ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中存入指定的鍵值對(duì)

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

優(yōu)點(diǎn):

代碼簡(jiǎn)潔,日志風(fēng)格統(tǒng)一,不需要在log打印中手動(dòng)拼寫traceId,即LOGGER.info("traceId:{} ", traceId)

暫時(shí)只能想到這一點(diǎn)

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

MDC使用

添加攔截器

publicclassLogInterceptorimplementsHandlerInterceptor{
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{
//如果有上層調(diào)用就用上層的ID
StringtraceId=request.getHeader(Constants.TRACE_ID);
if(traceId==null){
traceId=TraceIdUtil.getTraceId();
}

MDC.put(Constants.TRACE_ID,traceId);
returntrue;
}

@Override
publicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)
throwsException{
}

@Override
publicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)
throwsException{
//調(diào)用結(jié)束后刪除
MDC.remove(Constants.TRACE_ID);
}
}

修改日志格式

[TRACEID:%X{traceId}]%d{HHss.SSS}%-5level%class{-1}.%M()/%L-%msg%xEx%n

重點(diǎn)是%X{traceId},traceId和MDC中的鍵名稱一致

簡(jiǎn)單使用就這么容易,但是在有些情況下traceId將獲取不到

MDC 存在的問題

子線程中打印日志丟失traceId

HTTP調(diào)用丟失traceId

......丟失traceId的情況,來一個(gè)再解決一個(gè),絕不提前優(yōu)化

解決MDC存在的問題

子線程日志打印丟失traceId

子線程在打印日志的過程中traceId將丟失,解決方式為重寫線程池,對(duì)于直接new創(chuàng)建線程的情況不考略【實(shí)際應(yīng)用中應(yīng)該避免這種用法】,重寫線程池?zé)o非是對(duì)任務(wù)進(jìn)行一次封裝

線程池封裝類:ThreadPoolExecutorMdcWrapper.java

publicclassThreadPoolExecutorMdcWrapperextendsThreadPoolExecutor{
publicThreadPoolExecutorMdcWrapper(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,
BlockingQueueworkQueue){
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);
}

publicThreadPoolExecutorMdcWrapper(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,
BlockingQueueworkQueue,ThreadFactorythreadFactory){
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory);
}

publicThreadPoolExecutorMdcWrapper(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,
BlockingQueueworkQueue,RejectedExecutionHandlerhandler){
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler);
}

publicThreadPoolExecutorMdcWrapper(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,
BlockingQueueworkQueue,ThreadFactorythreadFactory,
RejectedExecutionHandlerhandler){
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);
}

@Override
publicvoidexecute(Runnabletask){
super.execute(ThreadMdcUtil.wrap(task,MDC.getCopyOfContextMap()));
}

@Override
publicFuturesubmit(Runnabletask,Tresult){
returnsuper.submit(ThreadMdcUtil.wrap(task,MDC.getCopyOfContextMap()),result);
}

@Override
publicFuturesubmit(Callabletask){
returnsuper.submit(ThreadMdcUtil.wrap(task,MDC.getCopyOfContextMap()));
}

@Override
publicFuturesubmit(Runnabletask){
returnsuper.submit(ThreadMdcUtil.wrap(task,MDC.getCopyOfContextMap()));
}
}

說明:

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

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

線程traceId封裝工具類:ThreadMdcUtil.java

publicclassThreadMdcUtil{
publicstaticvoidsetTraceIdIfAbsent(){
if(MDC.get(Constants.TRACE_ID)==null){
MDC.put(Constants.TRACE_ID,TraceIdUtil.getTraceId());
}
}

publicstaticCallablewrap(finalCallablecallable,finalMapcontext){
return()->{
if(context==null){
MDC.clear();
}else{
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try{
returncallable.call();
}finally{
MDC.clear();
}
};
}

publicstaticRunnablewrap(finalRunnablerunnable,finalMapcontext){
return()->{
if(context==null){
MDC.clear();
}else{
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try{
runnable.run();
}finally{
MDC.clear();
}
};
}
}

說明【以封裝Runnable為例】:

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

設(shè)置MDC中的traceId值,不存在則新生成,針對(duì)不是子線程的情況,如果是子線程,MDC中traceId不為null

執(zhí)行run方法

代碼等同于以下寫法,會(huì)更直觀

publicstaticRunnablewrap(finalRunnablerunnable,finalMapcontext){
returnnewRunnable(){
@Override
publicvoidrun(){
if(context==null){
MDC.clear();
}else{
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try{
runnable.run();
}finally{
MDC.clear();
}
}
};
}

重新返回的是包裝后的Runnable,在該任務(wù)執(zhí)行之前【runnable.run()】先將主線程的Map設(shè)置到當(dāng)前線程中【 即MDC.setContextMap(context)】,這樣子線程和主線程MDC對(duì)應(yīng)的Map就是一樣的了

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

設(shè)置MDC中的traceId值,不存在則新生成,針對(duì)不是子線程的情況,如果是子線程,MDC中traceId不為null

執(zhí)行run方法

HTTP調(diào)用丟失traceId

在使用HTTP調(diào)用第三方服務(wù)接口時(shí)traceId將丟失,需要對(duì)HTTP調(diào)用工具進(jìn)行改造,在發(fā)送時(shí)在request header中添加traceId,在下層被調(diào)用方添加攔截器獲取header中的traceId添加到MDC中

HTTP調(diào)用有多種方式,比較常見的有HttpClient、OKHttp、RestTemplate,所以只給出這幾種HTTP調(diào)用的解決方式

HttpClient:

實(shí)現(xiàn)HttpClient攔截器

publicclassHttpClientTraceIdInterceptorimplementsHttpRequestInterceptor{
@Override
publicvoidprocess(HttpRequesthttpRequest,HttpContexthttpContext)throwsHttpException,IOException{
StringtraceId=MDC.get(Constants.TRACE_ID);
//當(dāng)前線程調(diào)用中有traceId,則將該traceId進(jìn)行透?jìng)?if(traceId!=null){
//添加請(qǐng)求體
httpRequest.addHeader(Constants.TRACE_ID,traceId);
}
}
}

實(shí)現(xiàn)HttpRequestInterceptor接口并重寫process方法

如果調(diào)用線程中含有traceId,則需要將獲取到的traceId通過request中的header向下透?jìng)飨氯?/p>

為HttpClient添加攔截器

privatestaticCloseableHttpClienthttpClient=HttpClientBuilder.create()
.addInterceptorFirst(newHttpClientTraceIdInterceptor())
.build();

通過addInterceptorFirst方法為HttpClient添加攔截器

OKHttp:

實(shí)現(xiàn)OKHttp攔截器

publicclassOkHttpTraceIdInterceptorimplementsInterceptor{
@Override
publicResponseintercept(Chainchain)throwsIOException{
StringtraceId=MDC.get(Constants.TRACE_ID);
Requestrequest=null;
if(traceId!=null){
//添加請(qǐng)求體
request=chain.request().newBuilder().addHeader(Constants.TRACE_ID,traceId).build();
}
ResponseoriginResponse=chain.proceed(request);

returnoriginResponse;
}
}

實(shí)現(xiàn)Interceptor攔截器,重寫interceptor方法,實(shí)現(xiàn)邏輯和HttpClient差不多,如果能夠獲取到當(dāng)前線程的traceId則向下透?jìng)?/p>

為OkHttp添加攔截器

privatestaticOkHttpClientclient=newOkHttpClient.Builder()
.addNetworkInterceptor(newOkHttpTraceIdInterceptor())
.build();

調(diào)用addNetworkInterceptor方法添加攔截器

RestTemplate:

實(shí)現(xiàn)RestTemplate攔截器

publicclassRestTemplateTraceIdInterceptorimplementsClientHttpRequestInterceptor{
@Override
publicClientHttpResponseintercept(HttpRequesthttpRequest,byte[]bytes,ClientHttpRequestExecutionclientHttpRequestExecution)throwsIOException{
StringtraceId=MDC.get(Constants.TRACE_ID);
if(traceId!=null){
httpRequest.getHeaders().add(Constants.TRACE_ID,traceId);
}

returnclientHttpRequestExecution.execute(httpRequest,bytes);
}
}

實(shí)現(xiàn)ClientHttpRequestInterceptor接口,并重寫intercept方法,其余邏輯都是一樣的不重復(fù)說明

為RestTemplate添加攔截器

restTemplate.setInterceptors(Arrays.asList(newRestTemplateTraceIdInterceptor()));

調(diào)用setInterceptors方法添加攔截器

第三方服務(wù)攔截器:

HTTP調(diào)用第三方服務(wù)接口全流程traceId需要第三方服務(wù)配合,第三方服務(wù)需要添加攔截器拿到request header中的traceId并添加到MDC中

publicclassLogInterceptorimplementsHandlerInterceptor{
@Override
publicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{
//如果有上層調(diào)用就用上層的ID
StringtraceId=request.getHeader(Constants.TRACE_ID);
if(traceId==null){
traceId=TraceIdUtils.getTraceId();
}

MDC.put("traceId",traceId);
returntrue;
}

@Override
publicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)
throwsException{
}

@Override
publicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)
throwsException{
MDC.remove(Constants.TRACE_ID);
}
}

說明:

先從request header中獲取traceId

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

將生成的traceId存入MDC中

除了需要添加攔截器之外,還需要在日志格式中添加traceId的打印,如下:

[TRACEID:%X{traceId}]%d{HHss.SSS}%-5level%class{-1}.%M()/%L-%msg%xEx%n

需要添加%X{traceId}

審核編輯:湯梓紅

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • spring
    +關(guān)注

    關(guān)注

    0

    文章

    335

    瀏覽量

    14259
  • 日志
    +關(guān)注

    關(guān)注

    0

    文章

    129

    瀏覽量

    10593
  • Boot
    +關(guān)注

    關(guān)注

    0

    文章

    148

    瀏覽量

    35675
  • 線程
    +關(guān)注

    關(guān)注

    0

    文章

    501

    瀏覽量

    19580
  • SpringBoot
    +關(guān)注

    關(guān)注

    0

    文章

    172

    瀏覽量

    145

原文標(biāo)題:SpringBoot + MDC 實(shí)現(xiàn)全鏈路調(diào)用日志跟蹤

文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    2017雙11技術(shù)揭秘—雙十一海量數(shù)據(jù)下EagleEye的使命和挑戰(zhàn)

    升級(jí)提高了系統(tǒng)的穩(wěn)定性,實(shí)現(xiàn)了更好更快的輔助業(yè)務(wù)方定位及排查問題。圖3 系統(tǒng)架構(gòu)圖計(jì)算能力下沉早期的EagleEye在跟蹤以及數(shù)據(jù)統(tǒng)計(jì)都是基于明細(xì)
    發(fā)表于 12-29 13:54

    壓測(cè)一招搞定,阿里云性能測(cè)試鉑金版發(fā)布

    ,PTS宣布推出了基于阿里雙11壓測(cè)平臺(tái)的鉑金版。點(diǎn)此查看原文:http://click.aliyun.com/m/41269/阿里云性能測(cè)試(Performance Testing Service
    發(fā)表于 01-30 14:13

    API信息掌控,方便你的日志管理——阿里云推出API網(wǎng)關(guān)打通日志服務(wù)

    ,API網(wǎng)關(guān)每月前100萬次免費(fèi)。相當(dāng)于用戶每月前100萬次API調(diào)用日志存儲(chǔ)均免費(fèi),優(yōu)惠力度極大超越其他云廠商。該功能使用起來也十分簡(jiǎn)單,只需四步就可以輕松實(shí)現(xiàn): 阿里云API網(wǎng)關(guān)服務(wù)自2016年7
    發(fā)表于 02-06 15:24

    基于分布式調(diào)用監(jiān)控技術(shù)的全息排查功能

    作為鷹眼的商業(yè)化產(chǎn)品,用于APM監(jiān)控的阿里云業(yè)務(wù)實(shí)時(shí)監(jiān)控服務(wù) (ARMS) , 基于鷹眼的全息排查沉淀,近日推出了基于分布式調(diào)用監(jiān)控
    發(fā)表于 08-07 17:02

    基于SpringBoot mybatis方式的增刪改查實(shí)現(xiàn)

    SpringBoot mybatis方式實(shí)現(xiàn)增刪改查
    發(fā)表于 06-18 16:56

    監(jiān)控工具Skywalking使用指南

    國產(chǎn)監(jiān)控工具Skywalking
    發(fā)表于 09-03 14:26

    2021 OPPO開發(fā)者大會(huì):運(yùn)營

    2021 OPPO開發(fā)者大會(huì):運(yùn)營 2021 OPPO開發(fā)者大會(huì)上介紹了運(yùn)營,技術(shù)能
    的頭像 發(fā)表于 10-27 15:07 ?2234次閱讀
    2021 OPPO開發(fā)者大會(huì):<b class='flag-5'>全</b><b class='flag-5'>鏈</b><b class='flag-5'>路</b>運(yùn)營

    MDC300F UART 下發(fā)配置 日志調(diào)試

    文章目錄型號(hào)SOME/IP定義接收參數(shù)配置新建UART工程MDC發(fā)出的數(shù)據(jù)發(fā)送數(shù)據(jù)到MDC命令行運(yùn)行程序調(diào)用關(guān)系日志調(diào)試型號(hào)MDC300F,
    發(fā)表于 12-14 18:43 ?9次下載
    <b class='flag-5'>MDC</b>300F UART 下發(fā)配置 <b class='flag-5'>日志</b>調(diào)試

    手動(dòng)實(shí)現(xiàn)SpringBoot日志追蹤

    基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
    的頭像 發(fā)表于 12-15 15:04 ?1025次閱讀

    SpringBoot實(shí)現(xiàn)多線程

    SpringBoot實(shí)現(xiàn)多線程
    的頭像 發(fā)表于 01-12 16:59 ?1708次閱讀
    <b class='flag-5'>SpringBoot</b><b class='flag-5'>實(shí)現(xiàn)</b>多線程

    微服務(wù)循環(huán)依賴調(diào)用引發(fā)的血案

    順著測(cè)試匯報(bào)的出現(xiàn)問題的場(chǎng)景,跟蹤調(diào)用上相關(guān)服務(wù)的日志,發(fā)現(xiàn)出現(xiàn)了微服務(wù)之間循依賴調(diào)用。大致情況可以抽象如下所示(圖中所有
    的頭像 發(fā)表于 01-16 10:28 ?637次閱讀

    追蹤系統(tǒng)SkyWalking的原理

    內(nèi)部多個(gè)模塊,多個(gè)中間件,多臺(tái)機(jī)器的相互調(diào)用才能完成。在這一系列的調(diào)用中,可能有些是串行的,而有些是并行的。在這種情況下,我們?nèi)绾尾拍艽_定這整個(gè)請(qǐng)求調(diào)用了哪些應(yīng)用?哪些模塊?哪些節(jié)點(diǎn)?以及它們的先后順序和各部分的性能如何呢? 這
    的頭像 發(fā)表于 01-17 11:00 ?3886次閱讀

    SpringBoot+Vue實(shí)現(xiàn)網(wǎng)頁版人臉登錄、人臉識(shí)別案例解析

    Springboot,Mysql,JWT,VUE 2.X 等等技術(shù)實(shí)現(xiàn),主要功能點(diǎn):人臉列表CRUD,日志列表CRUD,基于自建人臉庫通過base64編碼方式存儲(chǔ)人臉圖片,通過調(diào)用騰訊
    發(fā)表于 02-23 15:36 ?990次閱讀

    SpringBoot+Vue實(shí)現(xiàn)網(wǎng)頁版人臉登錄、人臉識(shí)別

    技術(shù)點(diǎn):Springboot,Mysql,JWT,VUE 2.X 等等技術(shù)實(shí)現(xiàn),主要功能點(diǎn):人臉列表CRUD,日志列表CRUD,基于自建人臉庫通過base64編碼方式存儲(chǔ)人臉圖片,通過調(diào)用
    的頭像 發(fā)表于 03-07 09:27 ?961次閱讀

    Spring Boot如何實(shí)現(xiàn)日志追蹤

    ,各個(gè)接口的日志穿插,確實(shí)讓人頭大。 模糊匹配搜索日志能解決嗎? 能解決一點(diǎn)點(diǎn)。 但是不能完全呈現(xiàn)出整個(gè)相關(guān)的日志。 那要做到方便,很顯
    的頭像 發(fā)表于 05-16 11:33 ?2721次閱讀
    Spring Boot如何<b class='flag-5'>實(shí)現(xiàn)</b><b class='flag-5'>日志</b><b class='flag-5'>鏈</b><b class='flag-5'>路</b>追蹤