springboot 单机应用使用MDC生成唯一日志id

文章目录

    • 什么是MDC?
    • 如何使用MDC?
      • 简单封装一下,搞个工具类:
    • 单应用集成,就是在请求最开始设置MDC
      • filter建议,放到最前面
      • interceptor
      • 日志配置我使用的logback,只需要在标签最前面新增[%X{TRACE_ID}]即可
      • 针对@Scheduled注解:通过aop进行记录
      • 针对Rabbitmq怎么进行唯一TRACE_ID传递,简单点就是在所有发送之前将MDC的TRACE_ID塞到请求头里面,然后在接收消息的地方将请求头里面的TRACE_ID塞到MDC里面;
      • 其他组件之间的传递,道理都差不多,都是将trace_id传递下去.
    • 如何在日志记录中使用MDC?
    • 总结

在Spring Boot中,我们可以通过使用MDC Filter来轻松地将MDC集成到我们的应用程序中。本文将介绍如何使用Spring Boot MDC Filter来实现请求跟踪和日志记录。

什么是MDC?

MDC是一种日志记录技术,它允许我们将上下文信息存储在线程局部变量中,并在整个线程执行期间共享这些信息。这些上下文信息可以是任何东西,例如请求ID、用户ID、会话ID等等。MDC允许我们在跨多个线程和组件的日志记录中,轻松地将所有相关日志记录关联起来。
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。

当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

如何使用MDC?

org.slf4j.MDC的API:

  • clear() :移除所有MDC

  • get (String key) :获取当前线程MDC中指定key的值

  • getContext() :获取当前线程MDC的MDC

  • put(String key, Object o) :往当前线程的MDC中存入指定的键值对

  • remove(String key) :删除当前线程MDC中指定的键值对

简单封装一下,搞个工具类:


import org.slf4j.MDC;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;


public final class ThreadMdcUtil {
    private static final String TRACE_ID = "TRACE_ID";
    
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    public static void setTraceIdIfAbsent() {
        if (MDC.get(TRACE_ID) == null) {
            MDC.put(TRACE_ID, generateTraceId());
        }
    }

    public static void remove() {
        MDC.remove(TRACE_ID);
    }

    /**
     * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     *
     * @param callable
     * @param context
     * @param 
     * @return
     */
    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();
            }
        };
    }

    /**
     * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     *
     * @param runnable
     * @param context
     * @return
     */
    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();
            }
        };
    }
}

单应用集成,就是在请求最开始设置MDC

filter建议,放到最前面

import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class LogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ThreadMdcUtil.setTraceIdIfAbsent();
        try {
            filterChain.doFilter(request, response);
        } finally {
            ThreadMdcUtil.remove();
        }
    }
}

配置bean:

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(Integer.MIN_VALUE);
        return filterRegistrationBean;
    }

interceptor

import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class LogInterceptor implements HandlerInterceptor {

    private static final String TRACE_ID = "TRACE_ID";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        ThreadMdcUtil.setTraceIdIfAbsent();
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) {
        ThreadMdcUtil.remove();
    }

}

配置:

   @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor).addPathPatterns("/**");
    }

在Spring Boot中,我们可以使用MDC Filter来轻松地将MDC集成到我们的应用程序中。MDC Filter是一个Servlet过滤器,它允许我们在每个请求的开始时将上下文信息存储在MDC中,并在整个请求处理期间共享这些信息。在请求处理结束时,MDC Filter还会清除MDC中的上下文信息。

以下是一个简单的示例,展示了如何使用MDC Filter将请求ID存储在MDC中:

public class RequestIdMdcFilter implements Filter {

    private static final String REQUEST_ID_MDC_KEY = "requestId";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            String requestId = UUID.randomUUID().toString();
            MDC.put(REQUEST_ID_MDC_KEY, requestId);
            chain.doFilter(request, response);
        } finally {
            MDC.remove(REQUEST_ID_MDC_KEY);
        }
    }
}

日志配置我使用的logback,只需要在标签最前面新增[%X{TRACE_ID}]即可

针对@Scheduled注解:通过aop进行记录

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ScheduledTaskAspect {

    @Around("@annotation(org.springframework.scheduling.annotation.Scheduled)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 设置MDC上下文信息
        ThreadMdcUtil.setTraceIdIfAbsent();
        try {
            // 执行定时任务方法
            return joinPoint.proceed();
        } finally {
            ThreadMdcUtil.remove();
        }
    }
}

针对Rabbitmq怎么进行唯一TRACE_ID传递,简单点就是在所有发送之前将MDC的TRACE_ID塞到请求头里面,然后在接收消息的地方将请求头里面的TRACE_ID塞到MDC里面;

首先创建个@RabbitListener的切面,在执行完成之后删掉TRACE_ID

import lombok.extern.java.Log;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Log
public class RabbitMqListenerAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Object returnValue = methodInvocation.proceed();
        ThreadMdcUtil.remove();
        return returnValue;
    }

}

然后写个配置类:

import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class RabbitMQConfig {

    private static final String TRACE_ID = "TRACE_ID";

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        rabbitTemplate.setBeforePublishPostProcessors(message -> {
            // 在发送消息之前执行的逻辑
            message.getMessageProperties().setHeader(TRACE_ID, MDC.get(TRACE_ID));
            return message;
        });
        return rabbitTemplate;
    }

    @Bean(name = "rabbitListenerContainerFactory")
    public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(
            SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setAfterReceivePostProcessors(message -> {
            MDC.put(TRACE_ID, message.getMessageProperties().getHeader(TRACE_ID));
            return message;
        });
        factory.setAdviceChain(new RabbitMqListenerAdvice());
        configurer.configure(factory, connectionFactory);
        return factory;
    }

}

其他组件之间的传递,道理都差不多,都是将trace_id传递下去.

如何在日志记录中使用MDC?

一旦我们将MDC集成到我们的应用程序中,就可以在日志记录中使用它了。以下是一个简单的示例:
使用lombok注解@Slf4j,然后在类中使用log.info()就可以了

总结

在本文中,我们介绍了如何使用Spring Boot MDC Filter来实现请求跟踪和日志记录。通过使用MDC Filter,我们可以轻松地将MDC集成到我们的应用程序中,并在跨多个线程和组件的日志记录中轻松地将所有相关日志记录关联起来。如果您正在开发分布式系统,并且需要一种方式来跟踪请求并将所有相关日志记录关联起来,那么MDC是一个值得考虑的选择。

你可能感兴趣的:(springboot,spring,boot,java,后端)