利用Spring?boot+LogBack+MDC實現(xiàn)鏈路追蹤
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 中獲取所需的信息即可。
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中指定的鍵值對 。
MDC使用
1.攔截器
@Component public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果有上層調(diào)用就用上層的ID String traceId = request.getHeader(TraceIdUtil.TRACE_ID); if (StringUtil.isEmpty(traceId)) { TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId()); } else { TraceIdUtil.setTraceId(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é)束后刪除 TraceIdUtil.remove(); } }
2.工具類
public class TraceIdUtil { public static final String TRACE_ID = "requestId"; public static String getTraceId() { String traceId =(String) MDC.get(TRACE_ID); return traceId == null ? "" : traceId; } public static void setTraceId(String traceId) { MDC.put(TRACE_ID,traceId); } public static void remove() { MDC.remove(TRACE_ID); } public static void clear() { MDC.clear(); } public static String generateTraceId() { return UUID.randomUUID().toString().replace("-", ""); }
- 日志文件配置
<property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n" />
重點是%X{requestId},requestId和MDC中的鍵名稱保持一致。
MDC 存在的問題
至此基本的功能已經(jīng)實現(xiàn),但是存在一下幾個問題
- 多線程情況下,子線程中打印日志會丟失traceId.
- HTTP跨服務(wù)之間的調(diào)用丟失traceId.
子線程日志打印丟失traceId
問題重現(xiàn):
@LogAnnotation(model="用戶管理",func="查詢用戶信息",desc="根據(jù)用戶名稱") @GetMapping("getUserByName") public Result getUserByName(@RequestParam String name) { //主線程日志 logger.info("getUserByName paramter name:"+name); for(int i=0;i<5;i++) { //子線程日志 threadPoolTaskExecutor.execute(()->{ logger.info("child thread:{}",name); userService.getUserByName(name); }); } return Result.success(); }
運行結(jié)果:
2022-03-13 12:45:44.156 [http-nio-8089-exec-1] INFO [ec05a600ed1a4556934a3afa4883766a] c.s.fw.controller.UserController - getUserByName paramter name:1
2022-03-13 12:45:44.173 [Pool-A1] INFO [] c.s.fw.controller.UserController - child thread:1
從運行的結(jié)果來看,子線程打印日志,日志中的traceId信息已經(jīng)丟失。
解決方案:
子線程在打印日志的過程中traceId將丟失,解決方案為重寫線程池(對于直接new Thread 創(chuàng)建線程的情況不考略),實際開發(fā)中也需要禁止這種情況。
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor { private static final long serialVersionUID = 3940722618853093830L; @Override public void execute(Runnable task) { super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @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())); } }
因為Spring Boot ThreadPoolTaskExecutor 已經(jīng)對ThreadPoolExecutor進行封裝,只需要繼承ThreadPoolTaskExecutor重寫相關(guān)的執(zhí)行方法即可。
public class ThreadMdcUtil { public static void setTraceIdIfAbsent() { if (MDC.get(TraceIdUtil.TRACE_ID) == null) { MDC.put(TraceIdUtil.TRACE_ID, TraceIdUtil.generateTraceId()); } } 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); } //設(shè)置traceId setTraceIdIfAbsent(); try { runnable.run(); } finally { MDC.clear(); } }; } }
代碼說明:
- 判斷當(dāng)前線程對應(yīng)MDC的Map是否存在,如果存在則設(shè)置
- 設(shè)置MDC中的traceId值,不存在則新生成,如果是子線程,MDC中traceId不為null
- 執(zhí)行run方法
線程池配置
@Configuration public class ThreadPoolTaskExecutorConfig { //最大可用的CPU核數(shù) public static final int PROCESSORS = Runtime.getRuntime().availableProcessors(); @Bean public ThreadPoolExecutorMdcWrapper getExecutor() { ThreadPoolExecutorMdcWrapper executor =new ThreadPoolExecutorMdcWrapper(); executor.setCorePoolSize(PROCESSORS *2); executor.setMaxPoolSize(PROCESSORS * 4); executor.setQueueCapacity(50); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("Task-A"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); return executor; } }
重新運行結(jié)果發(fā)現(xiàn)子線程能夠正常獲取traceid信息進行跟蹤。
2022-03-13 13:19:30.688 [Task-A1] INFO [482929425cbc4476a4e7168615af7890] c.s.fw.controller.UserController - child thread:1
2022-03-13 13:19:31.003 [Task-A1] INFO [482929425cbc4476a4e7168615af7890] c.s.fw.service.impl.UserServiceImpl - name:1
HTTP調(diào)用丟失traceId
HTTP調(diào)用第三方服務(wù)接口時traceId丟失,需要在發(fā)送請求時在Request Header中添加traceId,在被調(diào)用方添加攔截器獲取header中的traceId添加到MDC中。
HTTP調(diào)用有多種方式,比較常見的有HttpClient、OKHttp、RestTemplate,以RestTemplate調(diào)用為例。
1.接口調(diào)用方
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { String traceId=MDC.get("requestId"); if(traceId!=null) { request.getHeaders().set("requestId", traceId); } else { request.getHeaders().set("requestId", UUID.randomUUID().toString().replace("-", "")); } return clientHttpRequestExecution.execute(request, body); } }
RestTemplate添加攔截器即可。
restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor())); 復(fù)制代碼
2.第三方服務(wù)需要添加攔截器
@Component public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果有上層調(diào)用就用上層的ID String traceId = request.getHeader(TraceIdUtil.TRACE_ID); if (StringUtil.isEmpty(traceId)) { TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId()); } else { TraceIdUtil.setTraceId(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é)束后刪除 TraceIdUtil.remove(); } }
其他HttpClient、OKHttp的實現(xiàn)方式與RestTemplate基本相同,這里就不一一列舉。 Spring boot +logback+MDC實現(xiàn)全鏈路跟蹤內(nèi)容已經(jīng)講完了
到此這篇關(guān)于利用Spring boot+LogBack+MDC實現(xiàn)鏈路追蹤的文章就介紹到這了,更多相關(guān)Spring boot 鏈路追蹤內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JAVA集成Freemarker生成靜態(tài)html過程解析
這篇文章主要介紹了JAVA集成Freemarker生成靜態(tài)html過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06利用java、js或mysql計算高德地圖中兩坐標(biāo)之間的距離
最近因為工作的需求,需要計算出高德地圖中兩個坐標(biāo)的距離,通過查找相關(guān)資料發(fā)現(xiàn)了多種實現(xiàn)的方法,下面這篇文章主要給大家介紹了關(guān)于利用java、js或mysql計算高德地圖中兩坐標(biāo)之間距離的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-10-10淺析Java中Apache BeanUtils和Spring BeanUtils的用法
這篇文章主要介紹了Java中Apache BeanUtils和Spring BeanUtils的用法,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11SpringBoot實現(xiàn)PDF添加水印的三種方法
本文主要介紹了SpringBoot實現(xiàn)PDF添加水印的三種方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07