SpringBoot如何實現全鏈路調用日志跟蹤


MDC介紹

簡介:

MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 、logback及log4j2 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個與當前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據

API說明:

  • clear() => 移除所有MDC

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

  • getContext() => 獲取當前線程MDC的MDC

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

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

優點:

  • 代碼簡潔,日志風格統一,不需要在log打印中手動拼寫traceId,即LOGGER.info("traceId:{} ", traceId)

暫時只能想到這一點

MDC使用

  • 添加攔截器

  1.  
    public class LogInterceptor implements HandlerInterceptor {
  2.  
        @Override
  3.  
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4.  
             //如果有上層調用就用上層的ID
  5.  
            String traceId = request.getHeader(Constants.TRACE_ID);
  6.  
             if (traceId == null) {
  7.  
                traceId = TraceIdUtil.getTraceId();
  8.  
            }
  9.  
     
  10.  
            MDC.put(Constants.TRACE_ID, traceId);
  11.  
             return true;
  12.  
        }
  13.  
     
  14.  
        @Override
  15.  
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
  16.  
                throws Exception {
  17.  
        }
  18.  
     
  19.  
        @Override
  20.  
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
  21.  
                throws Exception {
  22.  
             //調用結束后刪除
  23.  
            MDC.remove(Constants.TRACE_ID);
  24.  
        }
  25.  
    }
  • 修改日志格式

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

重點是%X{traceId},traceId和MDC中的鍵名稱一致 簡單使用就這么容易,但是在有些情況下traceId將獲取不到

MDC 存在的問題

  • 子線程中打印日志丟失traceId

  • HTTP調用丟失traceId ......丟失traceId的情況,來一個再解決一個,絕不提前優化

解決MDC存在的問題

子線程日志打印丟失traceId

子線程在打印日志的過程中traceId將丟失,解決方式為重寫線程池,對於直接new創建線程的情況不考略【實際應用中應該避免這種用法】,重寫線程池無非是對任務進行一次封裝

  • 線程池封裝類:ThreadPoolExecutorMdcWrapper.java

  1.  
    public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
  2.  
        public ThreadPoolExecutorMdcWrapper( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
  3.  
                                            BlockingQueue<Runnable> workQueue) {
  4.  
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
  5.  
        }
  6.  
     
  7.  
        public ThreadPoolExecutorMdcWrapper( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
  8.  
                                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
  9.  
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
  10.  
        }
  11.  
     
  12.  
        public ThreadPoolExecutorMdcWrapper( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
  13.  
                                            BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
  14.  
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
  15.  
        }
  16.  
     
  17.  
        public ThreadPoolExecutorMdcWrapper( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
  18.  
                                            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
  19.  
                                            RejectedExecutionHandler handler) {
  20.  
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
  21.  
        }
  22.  
     
  23.  
        @Override
  24.  
        public void execute(Runnable task) {
  25.  
            super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
  26.  
        }
  27.  
     
  28.  
        @Override
  29.  
        public <T> Future<T> submit(Runnable task, T result) {
  30.  
             return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
  31.  
        }
  32.  
     
  33.  
        @Override
  34.  
        public <T> Future<T> submit(Callable<T> task) {
  35.  
             return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
  36.  
        }
  37.  
     
  38.  
        @Override
  39.  
        public Future<?> submit(Runnable task) {
  40.  
             return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
  41.  
        }
  42.  
    }

說明:繼承ThreadPoolExecutor類,重新執行任務的方法 通過ThreadMdcUtil對任務進行一次包裝

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

  1.  
    public class ThreadMdcUtil {
  2.  
        public static void setTraceIdIfAbsent() {
  3.  
             if (MDC.get(Constants.TRACE_ID) == null) {
  4.  
                MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
  5.  
            }
  6.  
        }
  7.  
     
  8.  
        public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
  9.  
             return () -> {
  10.  
                 if (context == null) {
  11.  
                    MDC.clear();
  12.  
                } else {
  13.  
                    MDC.setContextMap(context);
  14.  
                }
  15.  
                setTraceIdIfAbsent();
  16.  
                try {
  17.  
                     return callable.call();
  18.  
                } finally {
  19.  
                    MDC.clear();
  20.  
                }
  21.  
            };
  22.  
        }
  23.  
     
  24.  
        public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
  25.  
             return () -> {
  26.  
                 if (context == null) {
  27.  
                    MDC.clear();
  28.  
                } else {
  29.  
                    MDC.setContextMap(context);
  30.  
                }
  31.  
                setTraceIdIfAbsent();
  32.  
                try {
  33.  
                    runnable.run();
  34.  
                } finally {
  35.  
                    MDC.clear();
  36.  
                }
  37.  
            };
  38.  
        }
  39.  
    }

說明【以封裝Runnable為例】:判斷當前線程對應MDC的Map是否存在,存在則設置 設置MDC中的traceId值,不存在則新生成,針對不是子線程的情況,如果是子線程,MDC中traceId不為null 執行run方法 代碼等同於以下寫法,會更直觀

  1.  
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
  2.  
             return new Runnable() {
  3.  
                @Override
  4.  
                public void run() {
  5.  
                     if (context == null) {
  6.  
                        MDC.clear();
  7.  
                    } else {
  8.  
                        MDC.setContextMap(context);
  9.  
                    }
  10.  
                    setTraceIdIfAbsent();
  11.  
                    try {
  12.  
                        runnable.run();
  13.  
                    } finally {
  14.  
                        MDC.clear();
  15.  
                    }
  16.  
                }
  17.  
            };
  18.  
        }

重新返回的是包裝后的Runnable,在該任務執行之前【runnable.run()】先將主線程的Map設置到當前線程中【 即MDC.setContextMap(context)】,這樣子線程和主線程MDC對應的Map就是一樣的了

HTTP調用丟失traceId

在使用HTTP調用第三方服務接口時traceId將丟失,需要對HTTP調用工具進行改造,在發送時在request header中添加traceId,在下層被調用方添加攔截器獲取header中的traceId添加到MDC中

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

HttpClient:

  • 實現HttpClient攔截器

  1.  
    public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
  2.  
        @Override
  3.  
        public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
  4.  
            String traceId = MDC.get(Constants.TRACE_ID);
  5.  
             //當前線程調用中有traceId,則將該traceId進行透傳
  6.  
             if (traceId != null) {
  7.  
                 //添加請求體
  8.  
                httpRequest.addHeader(Constants.TRACE_ID, traceId);
  9.  
            }
  10.  
        }
  11.  
    }

實現HttpRequestInterceptor接口並重寫process方法

如果調用線程中含有traceId,則需要將獲取到的traceId通過request中的header向下透傳下去

  • 為HttpClient添加攔截器

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

OKHttp:

  1.  
    private static CloseableHttpClient httpClient = HttpClientBuilder.create()
  2.  
                .addInterceptorFirst( new HttpClientTraceIdInterceptor())
  3.  
                .build();
  • 實現OKHttp攔截器

  1.  
    public class OkHttpTraceIdInterceptor implements Interceptor {
  2.  
        @Override
  3.  
        public Response intercept(Chain chain) throws IOException {
  4.  
            String traceId = MDC.get(Constants.TRACE_ID);
  5.  
            Request request = null;
  6.  
             if (traceId != null) {
  7.  
                 //添加請求體
  8.  
                request = chain.request().newBuilder().addHeader(Constants.TRACE_ID, traceId).build();
  9.  
            }
  10.  
            Response originResponse = chain.proceed(request);
  11.  
     
  12.  
             return originResponse;
  13.  
        }
  14.  
    }

實現Interceptor攔截器,重寫interceptor方法,實現邏輯和HttpClient差不多,如果能夠獲取到當前線程的traceId則向下透傳

  • 為OkHttp添加攔截器

  1.  
    private static OkHttpClient client = new OkHttpClient.Builder()
  2.  
                .addNetworkInterceptor( new OkHttpTraceIdInterceptor())
  3.  
                .build();

調用addNetworkInterceptor方法添加攔截器

RestTemplate:

  • 實現RestTemplate攔截器

  1.  
    public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
  2.  
        @Override
  3.  
        public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
  4.  
            String traceId = MDC.get(Constants.TRACE_ID);
  5.  
             if (traceId != null) {
  6.  
                httpRequest.getHeaders().add(Constants.TRACE_ID, traceId);
  7.  
            }
  8.  
     
  9.  
             return clientHttpRequestExecution.execute(httpRequest, bytes);
  10.  
        }
  11.  
    }

實現ClientHttpRequestInterceptor接口,並重寫intercept方法,其余邏輯都是一樣的不重復說明

  • 為RestTemplate添加攔截器

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

調用setInterceptors方法添加攔截器

第三方服務攔截器:

HTTP調用第三方服務接口全流程traceId需要第三方服務配合,第三方服務需要添加攔截器拿到request header中的traceId並添加到MDC中

  1.  
    public class LogInterceptor implements HandlerInterceptor {
  2.  
        @Override
  3.  
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4.  
             //如果有上層調用就用上層的ID
  5.  
            String traceId = request.getHeader(Constants.TRACE_ID);
  6.  
             if (traceId == null) {
  7.  
                traceId = TraceIdUtils.getTraceId();
  8.  
            }
  9.  
            
  10.  
            MDC.put( "traceId", traceId);
  11.  
             return true;
  12.  
        }
  13.  
     
  14.  
        @Override
  15.  
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
  16.  
                throws Exception {
  17.  
        }
  18.  
     
  19.  
        @Override
  20.  
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
  21.  
                throws Exception {
  22.  
            MDC.remove(Constants.TRACE_ID);
  23.  
        }
  24.  
    }

說明:

  • 先從request header中獲取traceId

  • 從request header中獲取不到traceId則說明不是第三方調用,直接生成一個新的traceId

  • 將生成的traceId存入MDC中

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

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

需要添加%X{traceId}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM