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("/**");
}
}
最终效果