SpringBoot+MDC реализует полное отслеживание журнала вызовов.

Java

статья, написанная ранеестатья, восторженный пользователь сети [Кидзо Кельвин] прокомментировал, что он все еще может быть запутанным в многопоточности, и рекомендуется печатать traceId через MDC, чтобы отследить вызов полной ссылки. В «Наггетс» все талантливы и красиво говорят, они мне очень нравятся. Nuggets помогают мне прогрессировать, а восторженные пользователи сети всегда могут внести предложения по улучшению.

написать впереди

В этой статье вы узнаете, что такое MDC, проблемы, существующие в приложении MDC, и как решить существующие проблемы.

Введение в MDC

Введение:

MDC (сопоставленный диагностический контекст) — это функция, предоставляемая log4j, logback и log4j2 для облегчения ведения журнала в многопоточных условиях.MDCможно рассматривать какХэш-таблица привязана к текущему потоку, вы можете добавить к нему пары ключ-значение. Контент, включенный в MDC, можетДоступ к коду, выполняемому в том же потоке.当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。 MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据

Описание API:
  • clear() => удаляет все MDC
  • get (String key) => Получить значение указанного ключа в MDC текущего потока
  • getContext() => Получить MDC текущего потока MDC
  • PUT (STRING Key, Object O) => Сохраняет указанное значение ключа в MDC текущего потока
  • Удалить (String Cook) => Удаляет пару значение ключа, указанную в текущем потоке MDC
преимущество:
  • Код лаконичен, стиль лога унифицирован, нет необходимости вручную прописывать 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 не будет получен

Проблемы с МДЦ

  • Журнал печати в дочернем потоке теряет traceId

  • HTTP-вызов теряет traceId

    ......Если traceId утерян, решите его один за другим и никогда не оптимизируйте его заранее.

Решить проблемы МДК

Отсутствует traceId в журнале дочернего потока

traceId дочернего потока будет утерян в процессе печати лога.Решение состоит в том, чтобы переписать пул потоков.В случае непосредственного создания нового потока это не рассматривается [такого использования следует избегать в практических приложениях] .Переписать пул потоков не более чем задача.один пакет

  • Класс пакета пула потоков: 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, метод повторного выполнения задачи
    • Оберните задачу один раз с помощью ThreadMdcUtil
  • Класс инструмента пакета thread 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 там устанавливается
    • Установите значение traceId в MDC. Если оно не существует, оно будет создано заново. В случае, если это не подпоток, если это подпоток, traceId в MDC не равен нулю.
    • Выполнить метод запуска

    Код эквивалентен следующему способу написания, который будет более интуитивным

    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.Перед выполнением задачи [runnable.run()] сначала устанавливает Map основного потока в текущий поток [т.е. MDC.setContextMap(context)], так что подпоток соответствует в основной поток MDC Карта такая же

HTTP-вызов теряет traceId

При использовании HTTP для вызова интерфейса обслуживания стороннего сервиса, TraceID будет потерян, и инструмент вызова HTTP должен быть изменен. При отправке добавьте TraceID на заголовок запроса, и добавьте перехватчик на более низкий уровень Callee на Получите TraceID в заголовке и добавьте его в MDC.

Существует множество способов выполнения HTTP-вызовов, наиболее распространенными являются HttpClient, OKHttp, RestTemplate, поэтому приводятся решения только для этих HTTP-вызовов.

HTTP-клиент:

  • Реализовать перехватчик 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);
        }
    }
}

Реализуйте интерфейс HttpRequestInterceptor и переопределите метод процесса.

Если вызывающий поток содержит traceId, полученный traceId должен быть прозрачно передан через заголовок в запросе.

  • Добавить перехватчик для HttpClient

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

    Добавьте перехватчик на httpClient через метод AddinterectorFirst

Okhttp:

  • Реализовать перехватчик 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;
        }
    }
    

    Реализовать перехватчик Interceptor и переписать метод перехватчика.Логика реализации аналогична HttpClient.Если удается получить traceId текущего потока, он будет прозрачно передан вниз.

  • Добавить перехватчик для OkHttp

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

    Вызовите метод addNetworkInterceptor, чтобы добавить перехватчик.

Шаблон отдыха:

  • Реализовать перехватчик 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);
        }
    }
    

    Реализовать интерфейс ClientHttpRequestInterceptor и переписать метод перехвата, остальная логика такая же и повторяться не будет.

  • Добавить перехватчик для RestTemplate

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

    Вызовите метод setInterceptors, чтобы добавить перехватчик

Перехватчики сторонних сервисов:

Полный процесс traceId HTTP-вызова к интерфейсу сторонней службы требует сотрудничества со сторонней службой. Сторонней службе необходимо добавить перехватчик, чтобы получить 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);
    }
}

инструкция:

  • Сначала получите TraceID из заголовка запроса
  • Если 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, поднимите звездочку, смиренно попрошайничая

Рекомендуемые статьи в прошлом

1. Пишите лог на запрос аспекта, удобнее скидывать фронт и бэкенды
2. Почему Alibaba отключает Executors для создания пулов потоков?

Справочная статья:
1. Используйте traceId для отслеживания всего журнала обработки запроса в проекте Java.
2. Введение в MDC — многопоточная практика управления журналами