# 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
**关注作者公众号【纠结的琐事】获取本项目源码**

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快速查找出同一个请求的所有日志

**细心的同学就会发现,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效果如下:

### 重写ThreadPoolExecutor效果如下:

**关注作者公众号【纠结的琐事】获取本项目源码**
