MDC实现链路追踪

MDC链路追踪

解决:线上日志查看过慢

cat -n info.log |grep "a415ad50dbf84e99b1b56a31aacd209c"
grep -10 'a415ad50dbf84e99b1b56a31aacd209c' info.log   (10是指上下10行)

一、配置文件

1.1 application.yml增加

server:
  servlet:
    context-path: /zz
  port: 8999
logging:
  config: classpath:logback-spring.xml

1.2 logback-spring.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
	<!--定义日志文件的存储地址 -->
	<property name="LOG_HOME" value="../logs" />

	<!--<property name="COLOR_PATTERN" value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta( %replace(%caller{1}){'\t|Caller.{1}0|\r\n', ''})- %gray(%msg%xEx%n)" />-->
	<!-- 控制台输出 -->
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符
			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>-->
			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{TRACE_ID} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n</pattern>
		</encoder>
	</appender>

	<!-- 按照每天生成日志文件 -->
	<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!--日志文件输出的文件名 -->
			<FileNamePattern>${LOG_HOME}/sso-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
			<!--日志文件保留天数 -->
			<MaxHistory>30</MaxHistory>
			<maxFileSize>10MB</maxFileSize>
		</rollingPolicy>
		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{TRACE_ID} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>
		</encoder>
	</appender>

	<!-- 生成 error html格式日志开始 -->
	<appender name="HTML" class="ch.qos.logback.core.FileAppender">
		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
			<!--设置日志级别,过滤掉info日志,只输入error日志-->
			<level>ERROR</level>
		</filter>
		<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
			<layout class="ch.qos.logback.classic.html.HTMLLayout">
				<pattern>%p%d%msg%M%F{32}%L</pattern>
			</layout>
		</encoder>
		<file>${LOG_HOME}/error-log.html</file>
	</appender>
	<!-- 生成 error html格式日志结束 -->

	<!-- 每天生成一个html格式的日志开始 -->
	<appender name="FILE_HTML" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
			<!--日志文件输出的文件名 -->
			<FileNamePattern>${LOG_HOME}/sso-%d{yyyy-MM-dd}.%i.html</FileNamePattern>
			<!--日志文件保留天数 -->
			<MaxHistory>30</MaxHistory>
			<MaxFileSize>10MB</MaxFileSize>
		</rollingPolicy>
		<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
			<layout class="ch.qos.logback.classic.html.HTMLLayout">
				<pattern>%p%d%msg%M%F{32}%L</pattern>
			</layout>
		</encoder>
	</appender>
	<!-- 每天生成一个html格式的日志结束 -->

	<!--myibatis log configure -->
	<logger name="com.apache.ibatis" level="TRACE" />
	<logger name="java.sql.Connection" level="DEBUG" />
	<logger name="java.sql.Statement" level="DEBUG" />
	<logger name="java.sql.PreparedStatement" level="DEBUG" />

	<!-- 日志输出级别 -->
	<root level="INFO">
		<appender-ref ref="STDOUT" />
		<appender-ref ref="FILE" />
		<appender-ref ref="HTML" />
		<appender-ref ref="FILE_HTML" />
	</root>

</configuration>

二、如何实现

采用得是拦截器方式,已每个线程为一个维度进行链路追踪
2.1 拦截器 WebMvcConfigurer添加拦截器

public class TraceInterceptor implements HandlerInterceptor {
    private static final String TRACE_ID = "TRACE_ID";
    /**
* MDC(Mapped Diagnostic Context)诊断上下文映射,是@Slf4j提供的一个支持动态打印日志信息的工具
* @param request
* @param response
* @param handler
* @return
*/
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tid = UUID.randomUUID().toString().replace("-", "");
        if (!StringUtils.isEmpty(request.getHeader("TRACE_ID"))) {
            tid = request.getHeader("TRACE_ID");
        }
        MDC.put(TRACE_ID, tid);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) {
        MDC.remove(TRACE_ID);
    }
}


@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public TraceInterceptor logInterceptor() {
        return new TraceInterceptor();
    }
    @Bean
    public SmsLimitationInterceptor getSmsLimitationInterceptor(){
        return new SmsLimitationInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getSmsLimitationInterceptor()).addPathPatterns("/**");
        registry.addInterceptor(logInterceptor()).addPathPatterns("/**");
    }

}

2.2 测试

    @PostMapping("doTest")
    public String doTest(@RequestParam("name") String name) throws InterruptedException {
        log.info("入参 name={}",name);
        testTrace();
        log.info("调用结束 name={}",name);
        return "Hello,"+name;
    }
    private void testTrace(){
        log.info("这是一行info日志");
        log.error("这是一行error日志");
        testTrace2();
    }
    private void testTrace2(){
        log.info("这也是一行info日志");

    }

三、多线程问题

使用多线程时,子线程无法进行链路追踪
将父线程的trackId传递下去给子线程

2023-07-22 10:53:26.442 3a164288b6aa43d8bc568b8fa5ca4bc2 [http-nio-8999-exec-7] INFO  com.steven.kz.shelf.controller.sso.DemoCon:88 - 主线程:1
2023-07-22 10:53:26.443 3a164288b6aa43d8bc568b8fa5ca4bc2 [http-nio-8999-exec-7] INFO  com.steven.kz.shelf.controller.sso.DemoCon:89 - 主线程:1
2023-07-22 10:53:26.443 3a164288b6aa43d8bc568b8fa5ca4bc2 [http-nio-8999-exec-7] INFO  com.steven.kz.shelf.controller.sso.DemoCon:90 - 主线程:1
2023-07-22 10:53:26.447  [demoExecutor-0] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:105 - 子线程 --start
2023-07-22 10:53:26.448  [demoExecutor-0] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:111 - 这是一行info日志
2023-07-22 10:53:26.448  [demoExecutor-0] ERROR c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:112 - 这是一行error日志
2023-07-22 10:53:26.448  [http-nio-8999-exec-7] INFO  com.steven.kz.shelf.config.filter.TokenFilter:66 - 无需鉴权已放行,url:/internal/external/doTestT
2023-07-22 10:53:26.449  [demoExecutor-0] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:116 - 这也是一行info日志
2023-07-22 10:53:26.449  [demoExecutor-0] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:107 - 子线程 --end

3.1 重写ThreadPoolTaskExecutor

public final class MyThreadPoolTaskExecutor  extends ThreadPoolTaskExecutor {
    public MyThreadPoolTaskExecutor() {
        super();
    }
    @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()));
    }
}
public final class ThreadMdcUtil {
    private static final String TRACE_ID = "TRACE_ID";
 
    // 获取唯一性标识
    public static String generateTraceId() {
        return UUID.randomUUID().toString();
    }
 
    public static void setTraceIdIfAbsent() {
        if (MDC.get(TRACE_ID) == null) {
            MDC.put(TRACE_ID, generateTraceId());
        }
    }
 
    /**
     * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     *
     * @param callable
     * @param context
     * @param <T>
     * @return
     */
    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();
            }
        };
    }
 
    /**
     * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     *
     * @param runnable
     * @param context
     * @return
     */
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

一定要使用我们重写后得类去创建线程池

@Configuration
@EnableAsync
public class ThreadPoolConfig {
    @Bean("MyExecutor")
    public Executor asyncExecutorT() {
        MyThreadPoolTaskExecutor executor = new MyThreadPoolTaskExecutor();
        //核心线程数5:线程池创建时候初始化的线程数
        executor.setCorePoolSize(5);
        //最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(5);
        //缓冲队列500:用来缓冲执行任务的队列
        executor.setQueueCapacity(500);
        //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("asyncJCccc");
        executor.initialize();
        return executor;
    }
}

扩展
ThreadPoolTaskExecutor是Spring框架对ThreadPoolExecutor的封装,它继承自ThreadPoolExecutor类,并提供了更多的功能和灵活性。下面是ThreadPoolTaskExecutor和ThreadPoolExecutor之间的主要区别:

  1. 封装程度:ThreadPoolTaskExecutor是对ThreadPoolExecutor的封装,提供了更高级别的抽象,以便于在Spring框架中使用。它提供了各种配置选项和自定义扩展点,更易于使用和管理。
  2. 配置灵活性:ThreadPoolTaskExecutor提供了更多的配置选项和属性,例如核心线程数、最大线程数、队列容量、线程池名称等。这些配置可以通过Spring配置文件或注解进行灵活配置,而ThreadPoolExecutor需要手动编写代码进行配置。
  3. 监控和管理:ThreadPoolTaskExecutor提供了丰富的监控和管理功能。它集成了Spring的任务执行框架,可以与其他Spring特性(如异步执行、定时任务)无缝集成。同时,它还提供了一些便利的方法和钩子,可以用于任务执行状态的监控、错误处理和自定义行为。
  4. 依赖关系:ThreadPoolTaskExecutor是Spring框架的一部分,因此在使用Spring框架的项目中无需额外导入依赖。而ThreadPoolExecutor是Java标准库中的类,需要手动引入java.util.concurrent包。

总的来说,ThreadPoolTaskExecutor是对ThreadPoolExecutor的增强和封装,提供了更多的功能和灵活性。它是在Spring框架中使用线程池的推荐方式,特别适用于基于Spring的应用程序开发。而ThreadPoolExecutor是Java标准库提供的原生线程池实现,可以直接在任何Java项目中使用。

2023-07-22 11:24:02.397 5bb69626459e42c1b0f88a3965b1de4e [http-nio-8999-exec-1] INFO  com.steven.kz.shelf.controller.sso.DemoCon:88 - 主线程:1
2023-07-22 11:24:02.397 5bb69626459e42c1b0f88a3965b1de4e [http-nio-8999-exec-1] INFO  com.steven.kz.shelf.controller.sso.DemoCon:89 - 主线程:1
2023-07-22 11:24:02.397 5bb69626459e42c1b0f88a3965b1de4e [http-nio-8999-exec-1] INFO  com.steven.kz.shelf.controller.sso.DemoCon:90 - 主线程:1
2023-07-22 11:24:07.805 5bb69626459e42c1b0f88a3965b1de4e [asyncJCccc1] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:105 - 子线程 --start
2023-07-22 11:24:07.811 5bb69626459e42c1b0f88a3965b1de4e [asyncJCccc1] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:111 - 这是一行info日志
2023-07-22 11:24:07.811 5bb69626459e42c1b0f88a3965b1de4e [asyncJCccc1] ERROR c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:112 - 这是一行error日志
2023-07-22 11:24:07.811 5bb69626459e42c1b0f88a3965b1de4e [asyncJCccc1] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:116 - 这也是一行info日志
2023-07-22 11:24:07.815 5bb69626459e42c1b0f88a3965b1de4e [asyncJCccc1] INFO  c.steven.kz.shelf.sso.service.impl.SmsServiceImpl:107 - 子线程 --end

将trackId封装到返回值里,便于快速查看日志

{
  "traceId": "5f1b68bfe19b49ad86548ea1e7112b29",
  "code": "0",
  "msg": "success",
  "success": true,
  "data": "Hello,哇"
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kkkouz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值