利用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


猜你喜欢
- LiveData概述LiveData 是一种可观察的数据存储器类: 与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循
- 做一个五子棋练练手,没什么特别的,再复习一下自定义View的知识,onMeasure,MeasureSpec , onDraw以及OnTou
- jol(java object layout)需要的依赖<dependency> <
- 前言之所以要写这篇关于C#反射的随笔,起因有两个:第一个是自己开发的网站需要用到其次就是没看到这方面比较好的文章。所以下定决心自己写一篇,废
- SpringMVC异常处理机制(一)项目前准备首先参照文章Spring课程工程构建+SpringMVC简介及其快速入门搭建项目搭建好一个项目
- 引言在App日益追求体验的时代,优秀的用户体验往往会使产品脱颖而出。今天我们就来介绍一种简单的滑动ListView来显示或者隐藏ToolBa
- 现在很多电脑提供了蓝牙支持,很多笔记本网卡也集成了蓝牙功能,也可以采用USB蓝牙方便的连接手机等蓝牙设备进行通信。操作蓝牙要使用类库InTh
- 很早以前为了快速达到效果,使用轮询实现了在线聊天功能,后来无意接触了socket,关于socket我的理解是进程间通信,首先要有服务器跟客户
- 本文实例讲述了如何计算(或者说,估算)一个Java对象占用的内存数量的方法。分享给大家供大家参考。具体分析如下:通常,我们谈论的堆内存使用的
- CyclicBarrier是什么CyclicBarrier是Java并发包中提供的一种同步工具类,它可以让多个线程在某个屏障处等待,直到所有
- 在读《Spring in Action》一书,读到Spring数据访问模板化的内容时,书中以乘坐飞机拖运行李为例,介绍了模板方法这一设计模式
- 本文实例为大家分享了Android实现语音播放与录音的具体代码,供大家参考,具体内容如下项目用到的技术点和亮点语音录音 (单个和列表)语音播
- 本文讲述的是Android中RelativeLayout、FrameLayout的用法。具体如下:RelativeLayout是一个按照相对
- 在了解Lambda表达式之前我们先来区分一下面向对象的思想和函数式编程思想的区别面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用
- 写在前面: 线程堆栈应该是多线程类应用程序非功能问题定位的最有效手段,可以说是杀手锏。线程堆栈最擅长与分析如下类型问题:系统无缘无故CPU过
- 在项目中遇到try...catch...语句,因为对Java异常处理机制的流程不是很清楚,导致对相关逻辑代码不理解。所以现在来总结Java异
- 本文通俗易懂的分析了C#中值类型和引用类型的区别。分享给大家供大家参考。具体分析如下:似乎“值类型和引用类型的区别”是今年面试的流行趋势,我
- 多线程经常访问同一资源可能造成什么问题竞态条件和死锁如果两个或多个线程访问相同的对象,或者访问不同步的共享状态 ,就会出现竞态条件;为了避免
- C# 的类型转换有显式转型 和 隐式转型 两种方式。显式转型:有可能引发异常、精确度丢失及其他问题的转换方式。需要使用手段进行转换操作。隐式
- 在使用之前先介绍一个并发需要用到的方法:CountDownLatchCountDownLatch(也叫闭锁)是一个同步协助类,允许一个或多个