MDC实现日志链路追踪

        开发过程中难免遇到需要查看日志来找出问题出在哪一环节的情况,而在实际情况中服务之间互相调用所产生的日志冗长且复杂,若是再加上同一时间别的请求所产生的日志,想要精准定位自己想要查看的日志就比较麻烦。为解决此问题,遂使用MDC日志追踪。

MDC简介及常用API

       简介

        MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

        API

  • clear() :移除所有MDC

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

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

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

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

  

实现

  最终效果

        本次测试用的prs-business和prs-data服务,从business服务调data服务,分别调用了四次。可以看到每次请求输出的日志前面都缀有[TRACEID:UUID 数字 域名]的标记,每次调用UUID不变,数字会+1,因为两个服务都是本地起的所以都是localhost,放在云上会变成真正的域名。 

  配置全局过滤器处理MDC

        首先创建过滤器,在第一次请求到达时生成traceId放入MDC中伴随这次请求的自始至终。或被其他服务调用时,拿到请求中传过来的traceId放入MDC中进行处理。

@Order(0)//告知spring创建对象的等级,0为最高级
@Component//声明component
@WebFilter(filterName = "requestIdFilter", urlPatterns = "/**")
public class RequestIdFilter implements Filter {//接收别的服务的请求时,拿出传过来的traceid,或者生成新的traceid,加入mdc,进行后续的链路追踪。

    private static final String REQUEST_ID = "requestId";//traceId的key统一设为requestId。

    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        try {
            //获取并设置RequestId
            MDC.put(REQUEST_ID, this.getRequestId(httpServletRequest));//向MDC存traceID
            chain.doFilter(httpServletRequest, httpServletResponse);
        } finally {
            MDC.clear();
        }
    }

    @Override
    public void destroy() {

    }
    /**
     * 获取请求RequestId
     *
     * @param httpServletRequest 请求
     * @return requestId
     */
    private String getRequestId(HttpServletRequest httpServletRequest) {
    
        String requestId = httpServletRequest.getHeader(REQUEST_ID);

        if (StringUtils.isBlank(requestId)) {//若请求中没有traceId,为空,意为是请求的最开始,则按照咱们咱们自己定的"UUID 0 域名"的格式生成一个全新的traceId,其中UUID就是以后用来查找日志的主要途径(关于这次请求的所有日志都会含有这个UUID,0表示是该次请求的最开始,域名的话可以区分是哪个服务,是测试版还是正式版。
          requestId = UUID.randomUUID().toString().replace("-", "")+" "+0+" "+httpServletRequest.getServerName();
        }else {//若请求中有traceId,不为空,意为是有别的服务调用了本服务发来了请求,需要对请求再处理一下。UUID不会变,承接传过来的UUID保证统一。中间的数字要+1,表示请求到达了新的接口。域名作用不变。
            String[] s = requestId.split(" ");//按照空格分隔进行处理
            s[1] = String.valueOf(Integer.parseInt(s[1]) + 1);
            requestId = s[0]+" "+s[1]+" "+httpServletRequest.getServerName();//处理好后重新拼接。
        }

        return requestId;
    }
}

RestTemplate添加拦截器

       HTTP调用第三方服务接口时traceId丢失,需要在发送请求时在Request Header中添加traceId,传给被调用方。

//调用别的服务时,给请求中加入已经生成好的traceid,发送请求,进行链路追踪
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {

    private static final String REQUEST_ID = "requestId";
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        String traceId= MDC.get(REQUEST_ID);
        request.getHeaders().set(REQUEST_ID, traceId);
        return execution.execute(request, body);
    }
} 

           代码中proxy调用其他服务时,需要将拦截器添加入RestTemplate 中。

public DefaultResponse test() {
    //String url = prsDataConfiguration.getEndpoint() + prsDataConfiguration.getTest();
    String url = "http://localhost:8006/mdc/mdctest";
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setInterceptors(Collections.singletonList(new RestTemplateTraceIdInterceptor()));//在proxy调用的时,将拦截器添加进去。
    return restTemplate.getForObject(url, DefaultResponse.class);
}

修改log.xml文件

          将下面标签添加到xml文件中的<Properties></Properties>标签中,次标签规定了日志中traceId的显示格式。注意:name="PATTERN" 看看名字是否有重复的,以防出现差错。

<property name="PATTERN">[TRACEID:%X{requestId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M(%L) - %msg%xEx%n</property>

  配置线程池 

        由于MDC底层实现是基于Threadlocal 实现的,由于在Filter配置的requestId,只属于主线程的上下文,如果在主线程中使用了线程池,开启了子线程,由于主、子线程MDC是相互隔离的,所以子线程中X-X{requestId}将无法获取到主线程filter中设置的requestId的,所以采用了一种比较巧妙的方式,因为每个线程最终都会执行 Runable,run方法,所以这里只需包装一下Runable,run方法结束之后清理MDC。

//重写线程池,使用ThreadMdcUtil重新进行封装,并在启动类中调用。为了将traceid加入到线程中,并显示在滚动日志中。
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, 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()));
    }

}
public class ThreadMdcUtil {
    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);
            }
            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);
            }
            try {
                runnable.run();
            } finally {
                MDC.clear();//最终清理MDC
            }
        };
    }
}
@Bean(name = "global Executor")//启动类中给封装好的线程池创建bean对象启动。
public ExecutorService getVendorInfoExecutor() {
    return new ThreadPoolExecutorMdcWrapper(10, 15, 0, TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(10), new ThreadPoolExecutor.CallerRunsPolicy());
}

 END

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值