MDC is introduced

MDC(Mapped Diagnostic Context) is a feature provided by LOG4J, LogBack, and Log4j2 to facilitate logging in multi-threaded conditions. MDC can be thought of as a hash table bound to the current thread to which you can add key-value pairs. The content contained in MDC can be accessed by code executing in the same thread. Children of the current thread inherit MDC content from their parent thread. To record logs, you only need to obtain the required information from the MDC.

API specification:

  • Clear () => Remove all MDC nodes
  • Get (String key) => Obtains the value of the specified key in the current MDC thread
  • GetContext () => Get the MDC of the current thread MDC
  • Put (String key, Object O) => Stores the specified key-value pair to the MDC of the current thread
  • Remove (String key) => Delete the key/value pairs specified in the current MDC thread.

The MDC use

  1. The interceptor
@Component public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, String traceId = request.getheader (traceidUtil.trace_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 {// Remove traceidUtil.remove (); }}Copy the code
  1. Utility class
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("-", "");
  }
Copy the code
  1. Log File Configuration
<property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n" />
Copy the code

The important point is that %X{requestId},requestId and MDC key names are the same.

Problems with the MDC

So far the basic function has been realized, but there are several problems

  • In multi-threaded cases, the traceId is lost when logs are printed on sub-threads.
  • The INVOCATION between HTTP services lost traceId. Procedure

Description The traceId of child thread logs is lost

Problem reproduction

@logAnnotation (model=" user management ",func=" query user information ",desc=" By user name ") @getMapping ("getUserByName") public Result GetUserByName (@requestParam String name) {// Mainline log logger.info("getUserByName paramter Name :"+name); for(int i=0; i<5; I++) {/ / the child thread log threadPoolTaskExecutor. Execute (() - > {logger. The info (" child thread: {} ", name); userService.getUserByName(name); }); } return Result.success(); }Copy the code

The results

2022-03-13 12:45:44. 156 [HTTP - nio - 8089 - exec - 1] INFO/ec05a600ed1a4556934a3afa4883766a C.S.F w.c ontroller. UserController 173 - getUserByName paramter name: 1 2022-03-13 12:45:44. [the Pool - A1] INFO [] C.S.F w.c ontroller. UserController - child thread:1Copy the code

The sub-thread generates logs, and traceId information in the logs is lost.

The solution

TraceId will be lost in the process of printing logs for child threads. The solution is to override the Thread pool (not mentioned in the case of threads created by direct new threads), which is also prohibited in practical development.

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

Since Spring Boot ThreadPoolTaskExecutor already encapsulates ThreadPoolExecutor, you only need to inherit the related execution methods of ThreadPoolTaskExecutor.

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);
            }
            //设置traceId
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

Copy the code

Code description:

  • Check whether the Map of the MDC for the current thread exists. If yes, set this parameter
  • If the traceId value does not exist, a new traceId value is generated. If the value is a child thread, the traceId value is not null
  • Execute the run method

Thread pool configuration

@ Configuration public class ThreadPoolTaskExecutorConfig {/ / maximum number of CPU cores available public static final ints 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; }}Copy the code

The child thread can obtain traceid information for tracing.

The 2022-03-13 13:19:30. 688 / Task - A1 INFO cbc4476a4e7168615af7890 [482929425] C.S.F w.c ontroller. UserController - child Thread: 1 2022-03-13 13:19:31. [Task - 003 A1] INFO cbc4476a4e7168615af7890 [482929425] C.S.F baron ervice. Impl. UserServiceImpl -  name:1Copy the code

Description HTTP invocation traceId is lost

The traceId is lost when the HTTP invokes the third-party service interface. You need to add the traceId to the Request Header when sending a Request, and add the interceptor to the invoked party to obtain the traceId in the Header and add the traceId to the MDC.

HTTP calls can be made in various ways, including HttpClient, OKHttp, and RestTemplate. The RestTemplate call is used as an example.

  1. Interface caller
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);
    }

}
Copy the code

RestTemplate adds an interceptor.

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));
Copy the code

2. Third-party services need to add interceptors.

@Component public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, String traceId = request.getheader (traceidUtil.trace_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 {// Remove traceidUtil.remove (); }}Copy the code

The other HttpClient and OKHttp implementations are basically the same as RestTemplate. Spring Boot + Logback +MDC to achieve full link tracing content is covered. In practice, I have also found some problems with the use of logs. The next chapter will explain the usage of logs in development.