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
标签:task,Java,MDC,ID,线程,链路,日志,public From: https://www.cnblogs.com/yk775879106/p/18055899