Java 实现全链路日志跟踪唯一ID

yk0205 / 2024-03-06 / 原文

Java 实现全链路日志跟踪唯一ID

日志痛点:
使用Spring-Aop切面的时候,只能切控制层或者服务层的开始位置与结束位置的数据(也就是请求出入参),对于逻辑日志无法定位跟踪

普通打印日志的时候是这样子的

1.如果参数里面没有seq传递过来

LOGGER.error("xxx不能为空" );

2.参数里面有seq传递过来

LOGGER.error("【" + seq + "】,xxx不能为空" );

第一种更简洁,第二种入侵了业务逻辑,并且每次都要拼接

解决方案:
1.简单的配置(异步线程会有点问题,log4j日志)
1)这些配置放到前置拦截器里面即可,控制层一进来就会赋值

//前置拦截器
String logUid = UUID.randomUUID().toString();
//org.apache.logging.log4j.ThreadContext
ThreadContext.put("logId", logId);

//后置拦截器
//在请求结束时需要清理logId
ThreadContext.clearMap();

2)日志打印关键 [logId::%X{logId}]
xml配置版

 <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="[logId::%X{logId}][%d{yyyy-MM-dd HH:mm:ss.SSS}] [%p] - %l - %m%n"/>
            <ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY" />
</console>

springboot

logging:
  pattern:
    #配置日志全链路跟踪 logId
    console: "[logId::%X{logId}] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%p] - %l - %m%n"

2.跟踪全链路,包括异步线程(logback日志)
关键点
1).MDC (org.slf4j.MDC)
2).拦截器 (主要是插入logId)
3).线程处理

拦截器代码,生成唯一logId

@Component
public class LogInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(LogConstant.TRACE_ID);
        if (traceId == null) {
            traceId = TraceIdUtil.getTraceId();
        }

        MDC.put(LogConstant.TRACE_ID, 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 {
        //调用结束后删除
        MDC.remove(LogConstant.TRACE_ID);
    }
}

获取日志链路ID工具类(利用UUID生成序列号)

public class TraceIdUtil {
    private TraceIdUtil() {
        throw new UnsupportedOperationException("Utility class");
    }
    /**
     * 获取traceId
     * @return
     */
    public static String getTraceId() {
        return UUID.randomUUID().toString().replace("-", "").toUpperCase();
    }
}

日志常量(常量,不可以new , 其实可以使用接口类定义)

public class LogConstant {
    private LogConstant(){
        throw new UnsupportedOperationException();
    }

    /**
     * 日志追踪ID
     */
    public static final String TRACE_ID = "traceId";
}

mdc线程处理器

public class ThreadMdcUtil {

    public static void setTraceIdIfAbsent() {
        if (MDC.get(LogConstant.TRACE_ID) == null) {
            //插入唯一日志ID
            MDC.put(LogConstant.TRACE_ID, TraceIdUtil.getTraceId());
        }
    }

    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);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }

}

线程配置类

@Slf4j
@Configuration
public class ExecutorConfig {

    @Bean
    @Primary
    public Executor asyncServiceExecutor() {
        log.info("start asyncServiceExecutor");
        ThreadPoolExecutorMdcWrapper executor = new ThreadPoolExecutorMdcWrapper();
        //配置核心线程数
        executor.setCorePoolSize(10);
        //配置最大线程数
        executor.setMaxPoolSize(200);
        //配置队列大小
        executor.setQueueCapacity(99999);
        //配置线程池中的线程的名称前缀
        executor.setThreadNamePrefix("async-service-");

        // 设置拒绝策略:当pool已经达到max size的时候,如何处理新任务
        // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //执行初始化
        executor.initialize();
        return executor;
    }
}

线程池配置

@Slf4j
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor {

    private void showThreadPoolInfo(String prefix){
        ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor();

        if(null==threadPoolExecutor){
            return;
        }

        log.info("{}, {},taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]",
                this.getThreadNamePrefix(),
                prefix,
                threadPoolExecutor.getTaskCount(),
                threadPoolExecutor.getCompletedTaskCount(),
                threadPoolExecutor.getActiveCount(),
                threadPoolExecutor.getQueue().size());
    }

    @Override
    public void execute(Runnable task) {
        showThreadPoolInfo("1. do execute");
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));

    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        showThreadPoolInfo("2. do execute");
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), startTimeout);

    }

    @Override
    public Future<?> submit(Runnable task) {
        showThreadPoolInfo("1. do submit");
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        showThreadPoolInfo("2. do submit");
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        showThreadPoolInfo("1. do submitListenable");
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        showThreadPoolInfo("2. do submitListenable");
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

拦截器

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private PreventRepeatSubmitInterceptor preventRepeatSubmitInterceptor;

    @Autowired
    private LogInterceptor logInterceptor;
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //设置允许跨域的路径
        registry.addMapping("/**") //映射地址
        .allowedOrigins("*")//允许跨域地址
        .allowedHeaders("*")
        .allowCredentials(true)
        .allowedMethods("GET", "POST")
        .maxAge(3600);
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //.excludePathPatterns("/wechatwork/**")  .addPathPatterns("/order/**")
        //防重复提交拦截器
        registry.addInterceptor(preventRepeatSubmitInterceptor);
        //日志拦截器
        registry.addInterceptor(logInterceptor);//.addPathPatterns("/**");
    }
}

原链接 https://blog.csdn.net/HX0326CSDN/article/details/121281618