background

In the current micro-service system, there are many service applications and complex call chains, so the difficulty of troubleshooting problems increases accordingly. When an exception occurs in the application, we need to quickly locate the problem log, which requires us to trace the request link and generate an ID that identifies the entire request life cycle when the request arrives in the system.

MDC (Mapped Diagnostic Contexts) Introduction

MDC is an important tool to realize distributed multithreaded log data transmission in Slf4J logging system. Users can print some context data at run time by using MDC. Currently only Log4J and LogBack provide native MDC support

Source code analysis

MDC maintains a Map

property in the thread context, which can be understood as a thread-level container. In logback, you can obtain the interface provided by the value MDC in the MDC context via %X{key}
,string>

public interface MDCAdapter {
    // Gets the value of the specified key in the current thread MDC context
    void put(String var1, String var2);
	// Go to the current thread MDC context
    String get(String var1);
	// Remove the key specified in the current thread MDC context
    void remove(String var1);
	// Clear the MDC context
    void clear(a);
	// Get the MDC context
    Map<String, String> getCopyOfContextMap(a);
	// Set the MDC context
    void setContextMap(Map<String, String> var1);
}
Copy the code

Logback implementation logback MDcAdapter

public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;
    final ThreadLocal<Integer> lastOperation = new ThreadLocal();

    public LogbackMDCAdapter(a) {}...Copy the code

You can see that LogbackMDC declares a map of type ThreadLocal. ThreadLocal provides thread-local instances. It differs from a normal variable in that each thread that uses it initializes a completely separate instance copy, meaning that ThreadLocal variables are isolated between threads and can be shared between methods or classes

LogbackMDCAdapter in the put ()

  public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        } else {
            Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
            Integer lastOp = this.getAndSetLastOperation(1);
            if (!this.wasLastOpReadOrNull(lastOp) && oldMap ! =null) {
                oldMap.put(key, val);
            } else {
                Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap); newMap.put(key, val); }}}Copy the code

Validates and sets the key values into the container of ThreadLocal

duplicateAndInsertNewMap()

 private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
        if(oldMap ! =null) {
            synchronized(oldMap) { newMap.putAll(oldMap); }}this.copyOnThreadLocal.set(newMap);
        return newMap;
    }
Copy the code

Create a thread-safe HashMap as a container and place it in a ThreadLocal

Scheme and Implementation

Declare filters to intercept requests

@Configuration
public class FilterConfig {
 @Bean
 public FilterRegistrationBean registFilter(a) {
     FilterRegistrationBean registration = new FilterRegistrationBean();
     registration.setFilter(new DyeFilter());
     registration.addUrlPatterns("/ *");
     registration.setName("DyeFilter");
     registration.setOrder(1);
     returnregistration; }}Copy the code

Build the context object and assign it to MDC

@WebFilter(filterName = "DyeFilter")
public class DyeFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest request = (HttpServletRequest) req;
        initGlobalContext(request);
        try {
            chain.doFilter(req, resp);
        } finally{ ContextUtil.clearContext(); }}private static void initGlobalContext(HttpServletRequest servletRequest) {
        GlobalContext context = new GlobalContext();
        String contextStr = servletRequest.getHeader(ContextConstant.REQUEST_CONTEXT);
        if(! StringUtils.isEmpty(contextStr)){ context = JSON.parseObject(contextStr,GlobalContext.class); }else{
            context.setTraceId(UUID.randomUUID().toString());
            context.setClientIp(IpUtil.getClientAddress(servletRequest));
        }
        ContextUtil.setCurrentContext(context);
    }

    @Override
    public void init(FilterConfig config){}@Override
    public void destroy(a) {}}Copy the code

The GlobalContext is constructed in the business entry, and in the subsequent invocation chain, the constructed GlobalContext is retrieved from the request header

public class ContextUtil {
    private static ThreadLocal<GlobalContext> currentThreadLocal = ThreadLocal.withInitial(GlobalContext::new);

    public static void setCurrentContext(GlobalContext context) {
        currentThreadLocal.set(context);
        String traceId = context.getTraceId();
        if(traceId ! =null && traceId.length() > 0 && MDC.get(ContextConstant.TRACK_ID) == null) { MDC.put(ContextConstant.TRACK_ID, traceId); }}public static GlobalContext getCurrentContext(a) {
       return currentThreadLocal.get();
    }

    public static void clearContext(a) { MDC.clear(); currentThreadLocal.remove(); }}Copy the code

Get the GlobalContext and pass it through the request header

public class FeignConfig {
 @Bean
 public RequestInterceptor header(a){
     return this::setRequestHeader;
 }
 private void setRequestHeader(RequestTemplate requestTemplate){
     GlobalContext context = ContextUtil.getCurrentContext();
     if(context! =null){ requestTemplate.header(ContextConstant.REQUEST_CONTEXT, JSONParser.quote(JSON.toJSONString(context))); }}}Copy the code

This is just an example of how to call a Service through Feign. There are many other ways to call a service to another service. The main idea is to pass the GlobalContext transparently to the next service

Configure variables in the MDC container in the Logback configuration file%X{trackId}

 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>% d {MM - dd yyyy - HH: MM: ss. The SSS} {trackId} % 5 level % X 15.15 t] [% % class. {39} % method [L] % : % m % n</pattern>
            <! -- Console should also use UTF-8, do not use GBK, otherwise Chinese garble -->
            <charset>UTF-8</charset>f
        </encoder>
    </appender>l
Copy the code

DEMO address: github.com/chenxuancod…