利用Spring boot+LogBack+MDC实现链路追踪
作者:剑圣无痕? 发布时间:2023-10-03 16:02:53
MDC介绍
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。
API说明
clear() => 移除所有MDC
get (String key) => 获取当前线程MDC中指定key的值
getContext() => 获取当前线程MDC的MDC
put(String key, Object o) => 往当前线程的MDC中存入指定的键值对
remove(String key) => 删除当前线程MDC中指定的键值对 。
MDC使用
1. *
@Component
public class LogInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果有上层调用就用上层的ID
String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
if (StringUtil.isEmpty(traceId))
{
TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
}
else
{
TraceIdUtil.setTraceId(traceId);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception
{
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
//调用结束后删除
TraceIdUtil.remove();
}
}
2.工具类
public class TraceIdUtil
{
public static final String TRACE_ID = "requestId";
public static String getTraceId()
{
String traceId =(String) MDC.get(TRACE_ID);
return traceId == null ? "" : traceId;
}
public static void setTraceId(String traceId)
{
MDC.put(TRACE_ID,traceId);
}
public static void remove()
{
MDC.remove(TRACE_ID);
}
public static void clear()
{
MDC.clear();
}
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
日志文件配置
<property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n" />
重点是%X{requestId},requestId和MDC中的键名称保持一致。
MDC 存在的问题
至此基本的功能已经实现,但是存在一下几个问题
多线程情况下,子线程中打印日志会丢失traceId.
HTTP跨服务之间的调用丢失traceId.
子线程日志打印丢失traceId
问题重现:
@LogAnnotation(model="用户管理",func="查询用户信息",desc="根据用户名称")
@GetMapping("getUserByName")
public Result getUserByName(@RequestParam String name)
{
//主线程日志
logger.info("getUserByName paramter name:"+name);
for(int i=0;i<5;i++)
{
//子线程日志
threadPoolTaskExecutor.execute(()->{
logger.info("child thread:{}",name);
userService.getUserByName(name);
});
}
return Result.success();
}
运行结果:
2022-03-13 12:45:44.156 [http-nio-8089-exec-1] INFO [ec05a600ed1a4556934a3afa4883766a] c.s.fw.controller.UserController - getUserByName paramter name:1
2022-03-13 12:45:44.173 [Pool-A1] INFO [] c.s.fw.controller.UserController - child thread:1
从运行的结果来看,子线程打印日志,日志中的traceId信息已经丢失。
解决方案:
子线程在打印日志的过程中traceId将丢失,解决方案为重写线程池(对于直接new Thread 创建线程的情况不考略),实际开发中也需要禁止这种情况。
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor
{
private static final long serialVersionUID = 3940722618853093830L;
@Override
public void execute(Runnable task)
{
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@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()));
}
}
因为Spring Boot ThreadPoolTaskExecutor 已经对ThreadPoolExecutor进行封装,只需要继承ThreadPoolTaskExecutor重写相关的执行方法即可。
public class ThreadMdcUtil
{
public static void setTraceIdIfAbsent() {
if (MDC.get(TraceIdUtil.TRACE_ID) == null)
{
MDC.put(TraceIdUtil.TRACE_ID, TraceIdUtil.generateTraceId());
}
}
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();
}
};
}
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
//设置traceId
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
代码说明:
判断当前线程对应MDC的Map是否存在,如果存在则设置
设置MDC中的traceId值,不存在则新生成,如果是子线程,MDC中traceId不为null
执行run方法
线程池配置
@Configuration
public class ThreadPoolTaskExecutorConfig
{
//最大可用的CPU核数
public static final int PROCESSORS = Runtime.getRuntime().availableProcessors();
@Bean
public ThreadPoolExecutorMdcWrapper getExecutor()
{
ThreadPoolExecutorMdcWrapper executor =new ThreadPoolExecutorMdcWrapper();
executor.setCorePoolSize(PROCESSORS *2);
executor.setMaxPoolSize(PROCESSORS * 4);
executor.setQueueCapacity(50);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("Task-A");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
重新运行结果发现子线程能够正常获取traceid信息进行跟踪。
2022-03-13 13:19:30.688 [Task-A1] INFO [482929425cbc4476a4e7168615af7890] c.s.fw.controller.UserController - child thread:1
2022-03-13 13:19:31.003 [Task-A1] INFO [482929425cbc4476a4e7168615af7890] c.s.fw.service.impl.UserServiceImpl - name:1
HTTP调用丢失traceId
HTTP调用第三方服务接口时traceId丢失,需要在发送请求时在Request Header中添加traceId,在被调用方添加 * 获取header中的traceId添加到MDC中。
HTTP调用有多种方式,比较常见的有HttpClient、OKHttp、RestTemplate,以RestTemplate调用为例。
1.接口调用方
public class RestTemplateTraceIdInterceptor implements
ClientHttpRequestInterceptor
{
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution clientHttpRequestExecution) throws IOException
{
String traceId=MDC.get("requestId");
if(traceId!=null)
{
request.getHeaders().set("requestId", traceId);
}
else
{
request.getHeaders().set("requestId", UUID.randomUUID().toString().replace("-", ""));
}
return clientHttpRequestExecution.execute(request, body);
}
}
RestTemplate添加 * 即可。
restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));
复制代码
2.第三方服务需要添加 *
@Component
public class LogInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果有上层调用就用上层的ID
String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
if (StringUtil.isEmpty(traceId))
{
TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
}
else
{
TraceIdUtil.setTraceId(traceId);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception
{
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
//调用结束后删除
TraceIdUtil.remove();
}
}
其他HttpClient、OKHttp的实现方式与RestTemplate基本相同,这里就不一一列举。 Spring boot +logback+MDC实现全链路跟踪内容已经讲完了
来源:https://juejin.cn/post/7074461710030995492
猜你喜欢
- 前言这几天听朋友说JPA很好用,根本不用写sql。我在想一个程序员不写sql还能叫程序员?而且越高级的工具封装越多的工具,可拓展性和效率就非
- import java.io.FileNotFoundException;import java.io.FileOutputStream;i
- Android 界面刷新 Android提供了Invalidate方法实现界面刷新,但是Invalidate不能直接在线程中调用,
- 一、背景在Idea中有些文件无需与远程git库同步,仅是本地使用,比如*.iml 、.idea(文件夹)等。如果不进行设置,那么每次提交列表
- 对某个类型中的方法进行拦截,然后加入固定的业务逻辑,这是AOP面向切面编程可以做的事,在springboot里实现aop的方法也有很多, s
- 线程池类图我们最常使用的Executors实现创建线程池使用线程主要是用上述类图中提供的类。在上边的类图中,包含了一个Executor框架,
- 现象: 1. 表面现象: 方法中输出的日志, 日志文件中找不到, 也没有任何报错(即@Async标注的方法没有执行, 也没有报错)2. 分析
- Spring框架是由于软件开发的复杂性而创建的。Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spri
- 概述java.lang.String 类代表字符串。Java程序中所有的字符串文字(例如"abc" )都可以被看作是实现
- 简单之美,springmvc,mybatis就是一个很好的简单集成方案,能够满足一般的项目需求。闲暇时间把项目配置文件共享出来,供大家参看:
- Java synchronized 关键字 可以将一个代码块或一个方法标记为同步代码块。同步代码块是指同一时间只能有一个线程执行的代码,并且
- 比如在类上使用该注解 @Alias("dDebtEntity")则在mapper.xml文件中resultType=&q
- 把char数组转换成String调用reverseStr()传入一个字符串"let’s"
- 在开发中,遇到了sql语句报错,但是并没有回滚的情况。经过几天的排查,终于找到了事务没有回滚的原因。原来的项目用的是informix的数据库
- @MapperScan包扫描的坑在使用通用mapper执行查询时,由于不太注意顺手就导了spring的包:import org.mybati
- 引言应用 Java 的开源库,编写一个搜索引擎,这个引擎能爬取一个网站的内容。并根据网页内容进行深度爬取,获取所有相关的网页地址和内容,用户
- 目录前言系统调用的分类同步回调实例异步回调实例基于Future的半异步小结前言先让我们通过一个生活中的场景来还原一下回调的场景:你遇到了一个
- Zookeeper和Eureka哪个更好?1、CAP理论一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求C:数据一致性
- 1 Struts2框架内部执行过程Structs请求过程源码分析参考链接https://www.jb51.net/article/22058
- 一:本文使用范围此文不仅仅局限于spring boot,普通的spring工程,甚至是servlet工程,都是一样的,只不过配置一些 * 的