小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Spring Boot + MDC 實現(xiàn)全鏈路調用日志跟蹤

 jacklopy 2023-01-10 發(fā)布于河北

寫在前面

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

MDC 介紹


簡介

MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 、logback及l(fā)og4j2 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個與當前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執(zhí)行的代碼所訪問。當前線程的子線程會繼承其父線程中的 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 中指定的鍵值對


優(yōu)點

代碼簡潔,日志風格統(tǒng)一,不需要在 log 打印中手動拼寫 traceId,即

LOGGER.info("traceId:{} ", traceId)

暫時只能想到這一點。

MDC 使用


添加攔截器

public class LogInterceptor implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        //如果有上層調用就用上層的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 { //調用結束后刪除 MDC.remove(Constants.TRACE_ID); }}
修改日志格式
<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的情況,來一個再解決一個,絕不提前優(yōu)化


解決 MDC 存在的問題


子線程日志打印丟失 traceId

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

線程池封裝類: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())); }}

說明:

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

    • 通過 ThreadMdcUtil 對任務進行一次包裝

線程 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(); } }; }}

說明(以封裝Runnable為例):

  • 判斷當前線程對應 MDC 的 Map 是否存在,存在則設置;

  • 設置 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();            }        }    };}

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

    • 判斷當前線程對應 MDC 的 Map 是否存在,存在則設置;

    • 設置 MDC 中的 traceId 值,不存在則新生成。針對不是子線程的情況,如果是子線程,MDC 中 traceId 不為 null;

    • 執(zhí)行 run 方法。


HTTP 調用丟失 traceId

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

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

HttpClient

實現(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);        //當前線程調用中有traceId,則將該traceId進行透傳        if (traceId != null) {            //添加請求體            httpRequest.addHeader(Constants.TRACE_ID, traceId);        }    }}

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

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

為 HttpClient 添加攔截器

private static CloseableHttpClient httpClient = HttpClientBuilder.create()            .addInterceptorFirst(new HttpClientTraceIdInterceptor())            .build();

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

OKHttp

實現(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; }}

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

為 OkHttp 添加攔截器

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

調用 addNetworkInterceptor 方法添加攔截器。

RestTemplate

實現(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); }}

實現(xiàn) ClientHttpRequestInterceptor 接口,并重寫 intercept 方法,其余邏輯都是一樣的,這里不做重復說明。

為 RestTemplate 添加攔截器

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

調用 setInterceptors 方法添加攔截器。

第三方服務攔截器

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

public class LogInterceptor implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        //如果有上層調用就用上層的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); }}

說明:

  • 先從 request header 中獲取t raceId;

  • 從 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}。

最后附:項目代碼,歡迎 fork 與 star,漲點小星星,卑微乞討。

https://github.com/TiantianUpup/springboot-log/tree/master/springboot-trace

轉自:何甜甜在嗎,



END

    本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多