How to implement full link call log tracking in SpringBoot? This method is called elegance

A previous article briefly introduced MDC. This time, we will have an in-depth understanding of MDC in combination with specific cases and specific problems in production.

Introduction to MDC

1. Introduction:

MDC (Mapped Diagnostic Context) is a function provided by log4j, logback and log4j2 to facilitate logging under multi-threaded conditions. MDC} can be regarded as a hash table bound to the current thread, to which key value pairs can be added. The content contained in MDC can be accessed by code executed in the same thread.

The child thread of the current thread inherits the contents of the MDC in its parent thread. When logging is required, you only need to obtain the required information from MDC. The content of MDC is saved by the program at an appropriate time. For a Web application, this data is usually saved at the beginning of the request being processed

2. API Description:

  • clear(): remove all MDC s

  • get (String key): get the value of the specified key in the MDC of the current thread

  • getContext(): get the MDC of the current thread

  • put(String key, Object o): stores the specified key value pair into the MDC of the current thread

  • remove(String key): deletes the key value pair specified in the MDC of the current thread

3. Advantages:

The code is concise and the log style is unified. There is no need to spell traceid manually in log printing, that is, logger info("traceId:{} ", traceId)

MDC usage

1. Add interceptor

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //If there is an upper layer call, use the upper layer 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 {
        //Delete after call
        MDC.remove(Constants.TRACE_ID);
    }
}

2. Modify log format

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

The key point is% X{traceId}, which is consistent with the key name in MDC

It is so easy to use, but in some cases, traceId cannot be obtained

Problems with MDC

  • traceId is missing from the print log in the child thread

  • Missing traceId for HTTP call

If the traceId is lost, solve it one by one, and never optimize it in advance

Solve problems in MDC

The trace ID is missing from the child thread log printing

The traceId of the child thread will be lost in the process of printing the log. The solution is to rewrite the thread pool. For the case of directly creating a new thread, do not omit [this usage should be avoided in practical application]. Rewriting the thread pool is nothing more than encapsulating the task

Thread pool encapsulation class: 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()));
    }
}

explain:

  • Inherit the ThreadPoolExecutor class and re execute the method of the task

  • Wrap the task once through ThreadMdcUtil

Thread traceId encapsulation tool class: 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();
            }
        };
    }
}

Description [take encapsulating Runnable as an example]:

  • Judge whether the MDC Map corresponding to the current thread exists, and set if it exists

  • Set the traceId value in the MDC. If it does not exist, it will be generated. If it is not a child thread, the traceId in the MDC will not be null

  • Execute run method

The code is equivalent to the following writing method, which will be more intuitive

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

The wrapped Runnable is returned again. Before the task is executed, [runnable.run()] first sets the Map of the main thread to the current thread [i.e. MDC.setContextMap(context)], so that the Map corresponding to the MDC of the sub thread and the main thread is the same

  • Judge whether the MDC Map corresponding to the current thread exists, and set if it exists

  • Set the traceId value in the MDC. If it does not exist, it will be generated. If it is not a child thread, the traceId in the MDC will not be null

  • Execute run method

Missing traceId for HTTP call

When using HTTP to call the third-party service interface, the traceId will be lost. The HTTP calling tool needs to be modified. When sending, add the traceId in the request header, add an interceptor in the lower callee, obtain the traceId in the header and add it to the MDC

There are many ways to call http. The more common ones are HttpClient, OKHttp and RestTemplate, so only the solutions to these HTTP calls are given

1,HttpClient:

Implement HttpClient Interceptor:

public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
    @Override
    public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
        String traceId = MDC.get(Constants.TRACE_ID);
        //If there is a traceId in the current thread call, the traceId will be transmitted transparently
        if (traceId != null) {
            //Add request body
            httpRequest.addHeader(Constants.TRACE_ID, traceId);
        }
    }
}

Implement the HttpRequestInterceptor interface and override the process method

If the calling thread contains traceId, you need to pass the obtained traceId down through the header in the request

Add interceptors for HttpClient:

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

Add an interceptor for HttpClient through the addInterceptorFirst method

2,OKHttp:

Implement OKHttp Interceptor:

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) {
            //Add request body
            request = chain.request().newBuilder().addHeader(Constants.TRACE_ID, traceId).build();
        }
        Response originResponse = chain.proceed(request);

        return originResponse;
    }
}

Implement the interceptor interceptor and rewrite the interceptor method. The implementation logic is similar to that of HttpClient. If the traceId of the current thread can be obtained, it will be transmitted downward

Add interceptors for OkHttp:

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

Call the addNetworkInterceptor method to add an interceptor

3,RestTemplate:

Implement RestTemplate Interceptor:

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);
    }
}

Implement the clienthttprequeinterceptor interface and rewrite the intercept method. The rest of the logic is the same without repeating the description

Add interceptors for RestTemplate:

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

Call the setInterceptors method to add an interceptor

4. Third party service Interceptor:

The whole process of HTTP calling the third-party service interface requires the cooperation of the third-party service. The third-party service needs to add an interceptor to get the traceId in the request header and add it to the MDC

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //If there is an upper layer call, use the upper layer 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);
    }
}

explain:

  • Get the traceId from the request header first

  • If the traceId cannot be obtained from the request header, it indicates that it is not a third-party call and a new traceId is directly generated

  • Store the generated traceId in MDC

In addition to adding interceptors, you also need to add traceId printing in the log format, as follows:

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

Note: you need to add% X{traceId}

Original address: How to implement full link call log tracking in SpringBoot? This method is called elegance!


If you think this article is helpful to you, please praise it, pay attention and support it

Keywords: Java Back-end Programmer

Added by JD-AM on Tue, 04 Jan 2022 15:38:43 +0200