Implementing log tracing with springboot

1, Introduction to MDC

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, and key value pairs can be added to it. 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) = > store the specified key value pair into the MDC of the current thread
  • Remove (string key) = > delete 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)

4, Code implementation

4.1 the request has no child thread

4.1.1 implement log interceptor
@Component
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("Entry interceptor");
    //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
    System.out.println("End of call");
    MDC.remove(Constants.TRACE_ID);
}
}
4.2.2 registered interceptors
@Configuration
public class MvcInterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
private LogInterceptor logInterceptor;

@Override
protected void addInterceptors(InterceptorRegistry registry) {
    // Multiple interceptors form an interceptor chain
    // addPathPatterns is used to add interception rules, / * * means to intercept all requests
    // excludePathPatterns user exclusion interception
    registry.addInterceptor(logInterceptor).addPathPatterns("/**");
    super.addInterceptors(registry);
}
}
4.3.3 log output format setting
<property name="LOG_PATTERN_CONSOLE">
	[TRACEID:%X{traceId}] %clr{%d{HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{[%15.15t]}{faint} %clr{%C{1.}(%M:%L)}{cyan} %clr{:}{faint} %m%n%xwEx
</property>

Note that the threadId in% X{traceId} should be the same as the key of MDC put in log interception

4.2 request with child thread

slf4j's MDC mechanism is internally implemented based on ThreadLocal. So we call MDC The request ID passed in by the put () method is only valid in the current thread. Therefore, the MDC data set in the main thread cannot be obtained in its child thread (thread pool).

Official recommendations

1) call MDC. before the parent thread creates a new child thread. The getcopyofcontextmap () method takes out the MDC content and passes it to the child thread

2) The child thread calls MDC before performing the operation The setcontextmap () method sets the MDC content of the parent thread to the child thread

Code implementation:

1) Use Aop to intercept requests, the same as above

2) log4j log configuration is the same as above

3) Decorator mode decorates sub threads in two ways

4.2.1 implementation mode 1 - Decorative Runnable

Use the decorator mode to decorate the Runnable interface, and create the MDCRunnable class to decorate the Runnable interface.

When creating the MDCRunnable class, save the MDC value of the current thread, and then execute the run() method

public class MDCRunnable implements Runnable{

private Runnable runnable;

/**
 * Saves the MDC value of the current main thread
 */
private final Map<String, String> mainMdcMap;

public MDCRunnable(Runnable runnable) {
    this.runnable = runnable;
    this.mainMdcMap = MDC.getCopyOfContextMap();
}

@Override
public void run() {
    // Assign the MDC value of the parent thread to the child thread
    for (Map.Entry<String, String> entry : mainMdcMap.entrySet()) {
        MDC.put(entry.getKey(), entry.getValue());
    }
    // Execute the decorated thread run method
    runnable.run();
    // Remove MDC value at the end of execution
    for (Map.Entry<String, String> entry : mainMdcMap.entrySet()) {
       MDC.remove(entry.getKey());
    }
  }
}

Test: use MDCRunnable instead of Runnable

@PostMapping("/test")
public String test(@RequestBody String str){
    log.info("get into test Request method");
    SmsThreadPoolUtil.getInstance().execute(new MDCRunnable(new Runnable() {
        @Override
        public void run() {
            log.info("Sub thread 1 executes the task");
        }
    }));
    executorService.execute(new MDCRunnable(new Runnable() {
        @Override
        public void run() {
            log.info("Sub thread 2 executes the task");
        }
    }));

    log.info("test End of method execution");
    return str;
}
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.857 DEBUG [nio-9999-exec-1] o.s.c.l.LogFormatUtils(traceDebug:91) : Read "application/json;charset=UTF-8" to ["{
    "str":"11111111"
}"]
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.863  INFO [nio-9999-exec-1] c.t.c.TestController(test:41) : get into test Request method
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.865  INFO [pool-3-thread-1] c.t.c.TestController$1(run:45) : Sub thread 1 executes the task
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.865  INFO [nio-9999-exec-1] c.t.c.TestController(test:55) : test End of method execution
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.865  INFO [pool-2-thread-1] c.t.c.TestController$2(run:51) : Sub thread 2 executes the task
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.871 DEBUG [nio-9999-exec-1] o.s.w.s.m.m.a.AbstractMessageConverterMethodProcessor(writeWithMessageConverters:268) : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.872 DEBUG [nio-9999-exec-1] o.s.c.l.LogFormatUtils(traceDebug:91) : Writing ["{
    "str":"11111111"
}"]
4.2.2 implementation mode 2 - decorative thread pool
public class MDCThreadPoolExecutor extends ThreadPoolExecutor {
public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}

public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}

public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}

public MDCThreadPoolExecutor(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(final Runnable runnable) {
    // The content in the MDC of the parent thread must be obtained before the run method. Otherwise, when the asynchronous thread executes, the value in the MDC may have been cleared, and null will be returned at this time
    final Map<String, String> context = MDC.getCopyOfContextMap();
    super.execute(new Runnable() {
        @Override
        public void run() {
            // Pass the MDC content of the parent thread to the child thread
            MDC.setContextMap(context);
            try {
                // Perform asynchronous operations
                runnable.run();
            } finally {
                // Empty MDC content
                MDC.clear();
            }
        }
    });
  }
}

Test: replace ThreadPoolExecutor with MDCThreadPoolExecutor

public class SmsThreadPoolUtil {
	//Define type
	private static ThreadPoolExecutor pool=new MDCThreadPoolExecutor(10, 20,
			10, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(10000), new RejectedPoolUtils());
	
	public static ThreadPoolExecutor getInstance() {
		return pool;
	}
}

@Service
@Slf4j
public class RejectedPoolUtils implements RejectedExecutionHandler{
	@Override
	public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
		log.info("Maximum number of queues reached");
	}
}
@PostMapping("/test")
public String test(@RequestBody String str){
    log.info("get into test Request method");
    SmsThreadPoolUtil.getInstance().execute(new Runnable() {
        @Override
        public void run() {
            log.info("Sub thread 1 executes the task");
        }
    });
    log.info("test End of method execution");
    return str;
}

Keywords: Java Spring Spring Boot

Added by alexk1781 on Wed, 19 Jan 2022 09:38:44 +0200