美文网首页java开发技术
springboot+MDC打造日志链条

springboot+MDC打造日志链条

作者: source201 | 来源:发表于2022-03-17 16:38 被阅读0次

    场景

    在系统中,多个用户的并发请求或者多个线程的日志会交缠在一起,使得对日志的分析造成较大的困难,若能够将单个用户的请求,或者单个线程的请求用以不同的标识,在日志中标识出来,则能够更好的分析日志。

    思路

    要对每个请求的日志加上唯一性标识,如果纯靠手动加入,工作量大,修改麻烦,出现遗漏等等问题。而通过MDC可以解决上述问题。

    本文是通过MDC来对每一个请求所经过代码加入唯一性标识,其中包含父子线程标识统一,feign跨服务调用日志统一等,来打造该请求的完整日志链条。

    日志过滤器

    首先通过过滤器来对每个请求加入唯一性标识。

    import cn.hutool.core.util.StrUtil;
    import com.demo.util.RequestIdMdcUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.slf4j.MDC;
    import org.springframework.core.Ordered;
    import org.springframework.core.annotation.Order;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @WebFilter(filterName = "LogFilter", urlPatterns = "/*")
    @Slf4j
    public class LogFilter implements Filter {
    
        @Override
        public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) throws IOException, ServletException {
            final HttpServletResponse response = (HttpServletResponse) res;
            final HttpServletRequest reqs = (HttpServletRequest) req;
            String requestId = reqs.getHeader(RequestIdMdcUtil.REQUEST_ID);
            if (StrUtil.isBlank(requestId)) {
                requestId = RequestIdMdcUtil.getRequestId();
            }
    
            MDC.put(RequestIdMdcUtil.REQUEST_ID, requestId);
            log.info("LogFilter触发request_id:{}",MDC.get(RequestIdMdcUtil.REQUEST_ID));
            try{
                chain.doFilter(req, res);
            }finally {
                MDC.remove(RequestIdMdcUtil.REQUEST_ID);
            }
        }
    
        @Override
        public void init(final FilterConfig filterConfig) {
            log.info("LogFilter初始化");
        }
    
        @Override
        public void destroy() {
            MDC.remove(RequestIdMdcUtil.REQUEST_ID);
        }
    }
    

    唯一标识工具类

    import org.slf4j.MDC;
    
    import java.util.Map;
    import java.util.UUID;
    import java.util.concurrent.Callable;
    
    public class RequestIdMdcUtil {
    
        public static final String REQUEST_ID = "request_id";
    
        // 获取唯一性标识
        public static  String getRequestId(){
            return UUID.randomUUID().toString();
        }
    
        public static void setRequestIdIfAbsent() {
            if (MDC.get(REQUEST_ID) == null) {
                MDC.put(REQUEST_ID, getRequestId());
            }
        }
        // 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
        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);
                }
                setRequestIdIfAbsent();
                try {
                    return callable.call();
                } finally {
                    MDC.clear();
                }
            };
        }
    
        // 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
        public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
            return () -> {
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                setRequestIdIfAbsent();
                try {
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            };
        }
    }
    

    日志格式设置

    logging.pattern.console=[REQUESTID:%X{request_id}]%d{yyyy-MM-dd HH:mm:ss} [%t] [%-5level] %logger{96} [%M:%L] - %msg%n
    

    以上设置,即完成了简单场景的设置

    使用了线程池的场景

    使用了线程池的场景,主要问题是,MDC中的数据不同线程是不共享的,因此需要将父线程中的MDC数据传入子线程中。

    构建一个ThreadPoolTaskExecutor的子类

    import com.demo.util.RequestIdMdcUtil;
    import org.slf4j.MDC;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.Future;
    
    /**
     * @description 封装线程池任务执行器,在任务提交时,会将父线程的request_id,带入子线程
     * @author 
     * @date 2022/3/3 17:35
     */
    public class ThreadPoolMdcTaskExecutor extends ThreadPoolTaskExecutor {
    
    
        public ThreadPoolMdcTaskExecutor() {
            super();
        }
    
    
    
        @Override
        public void execute(Runnable task) {
            super.execute(RequestIdMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    
    
        @Override
        public <T> Future<T> submit(Callable<T> task) {
            return super.submit(RequestIdMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    
        @Override
        public Future<?> submit(Runnable task) {
            return super.submit(RequestIdMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
        }
    }
    

    在新建线程池的时候,new的是子类ThreadPoolMdcTaskExecutor即可。

    跨服务feign调用场景

    跨服务调用场景和父子线程场景类似,需要将MDC中的request_id加入到header中,然后在另一个服务中的过滤器会取出它,加入到MDC中。

    @Configuration
    @Slf4j
    public class FeignConfig implements RequestInterceptor {
    
        @Override
        public void apply(final RequestTemplate template) {
            //将MDC中request_id传入header
            template.header(RequestIdMdcUtil.REQUEST_ID, MDC.get(RequestIdMdcUtil.REQUEST_ID));
        }
    
    }
    

    相关文章

      网友评论

        本文标题:springboot+MDC打造日志链条

        本文链接:https://www.haomeiwen.com/subject/tbnvdrtx.html