# open-trace **Repository Path**: open-cloud-framework/open-trace ## Basic Information - **Project Name**: open-trace - **Description**: SpringBoot+slf4j实现全链路调用日志跟踪 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2021-03-04 - **Last Updated**: 2021-07-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README **关注作者公众号【纠结的琐事】获取本项目源码** ![](https://img2020.cnblogs.com/blog/1666887/202103/1666887-20210312210745626-49925775.png) SpringBoot中除了常见的分布式链路跟踪系统zipkin、skywalking等,如果需要快速定位一次请求的所有日志,那么该如何实现?实际slf4j提供了MDC(Mapped Diagnostic Contexts)功能,支持用户定义和修改日志的输出格式以及内容。本文将介绍 Tracer集成的slf4j MDC功能,方便用户在只简单修改日志配置文件的前提下输出当前 Tracer 上下文 TraceId。 ## MDC介绍 MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。 ## springboot中如何使用 ### 添加拦截器 ````java public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String tid = UUID.randomUUID().toString().replace("-", ""); MDC.put(CloudConstant.MDC_TRACE, tid); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { MDC.remove(CloudConstant.MDC_TRACE); } } ```` ### 注册拦截器 ````java @Configuration public class WebInterceptorAdapter implements WebMvcConfigurer { @Bean public LogInterceptor logInterceptor() { return new LogInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logInterceptor()); } } ```` ### 修改日志输出格式,添加%X{traceId},traceId和MDC中的键名称一致 ````java %date %-5level ${PID:- } [%thread] [%X{tid}] : /*[%logger{50}:%line] %msg*/%n ```` ### 添加一个controller调用测试 ````java @RestController @RequestMapping("trace") @Slf4j public class TestTraceController { @GetMapping("traceLog") public String traceLog() { log.info("---接口调用了---"); traceService(); return "success"; } private void traceService(){ log.error("## 执行traceService方法"); } } ```` ### 日志打印如下,我们可以通过traceId快速查找出同一个请求的所有日志 ![](https://img2020.cnblogs.com/blog/1666887/202103/1666887-20210304211235008-981776106.png) **细心的同学就会发现,MDC还是存在一些问题** - 在子线程中打印日志丢失traceId - HTTP调用丢失traceId ## 解决思路 子线程在打印日志的过程中traceId将丢失,解决方式为重写线程池,将主线程的traceId继续传递到子线程中。当然,对于直接new创建线程的情况不考略【实际应用中应该避免这种用法】。 ### 继承ThreadPoolExecutor,重写执行任务的方法 ````java public final class OverrideThreadPoolExecutor extends ThreadPoolExecutor { @Override public void execute(Runnable task) { super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @Override public Future submit(Runnable task, T result) { return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result); } @Override public Future submit(Callable task) { return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @Override public Future submit(Runnable task) { return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } } ```` ### 封装ThreadMdcUtil工具类 **以封装Callable为例:** - 判断当前线程对应MDC的Map是否存在,如果存在则设置子线程的ContextMap为当前线程的; - 如果不存在,则重新生成traceId; - 执行run方法 ````java public final class ThreadMdcUtil { public static void setTraceIdIfAbsent() { if (MDC.get(TraceConstant.MDC_TRACE) == null || MDC.get(TraceConstant.MDC_TRACE).length() == 0) { String tid = UUID.randomUUID().toString().replace("-", ""); MDC.put(TraceConstant.MDC_TRACE, tid); } } public static Callable wrap(final Callable callable, final Map context) { return () -> { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } setTraceIdIfAbsent(); try { return callable.call(); } finally { MDC.clear(); } }; } } ```` ## 测试子线程中traceId的传递,本项目中ExecutorService已经重写了线程池 ````java @RestController @RequestMapping("trace") @Slf4j @AllArgsConstructor public class TestTraceController { private final ExecutorService executorService; @GetMapping("traceLog") public String traceLog() { log.info("---接口调用了---"); traceService(); asyncTrace(); return "success"; } private void traceService(){ log.error("## 执行traceService方法"); } private void asyncTrace(){ CompletableFuture.runAsync(()->{ log.info("执行线程池中的方法asyncTrace,未重写了线程池"); }, executorService); } } ```` ### 未重写ThreadPoolExecutor效果如下: ![](https://img2020.cnblogs.com/blog/1666887/202103/1666887-20210304214604877-1305560627.png) ### 重写ThreadPoolExecutor效果如下: ![](https://img2020.cnblogs.com/blog/1666887/202103/1666887-20210304214611639-706517510.png) **关注作者公众号【纠结的琐事】获取本项目源码** ![](https://images.gitee.com/uploads/images/2021/0304/215650_274126ca_4787103.png)