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

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("/**");
	}



}

最终效果
在这里插入图片描述

  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Plumelog一个简单易用的java分布式日志组件。支持百亿级别,日志从搜集到查询,不用去文件中翻阅日志方便快捷,支持查询一个调用链的日志,支持链路追踪,查看调用链耗时情况,在分布式系统中也可以查询关联日志,能够帮助快速定位问题,简单易用,没有代码入侵,查询界面友好,高效,方便,只要你是java系统,不要做任何项目改造,接入直接使用,日志不落本地磁盘,无需关心日志占用应用服务器磁盘问题,觉得项目好用帮忙点个星星,您的star是我们前进的动力。 Plumelog功能介绍: 1、无入侵的分布式日志系统,基于log4j、log4j2、logback搜集日志设置链路ID,方便查询关联日志 2、基于elasticsearch作为查询引擎 3、高吞吐,查询效率高 4、程不占应用程序本地磁盘空间,免维护;对于项目透明,不影响项目本身运行 5、无需修改老项目,引入直接使用,支持dubbo,支持springcloud Plumelog架构: plumelog-core:核心组件包含日志搜集端,负责搜集日志并推送到kafka,redis等队列 plumelog-server:负责把队列中的日志日志异步写入到elasticsearch plumelog-ui:前端展示,日志查询界面 plumelog-demo:基于springboot的使用案例 Plumelog使用方法: 自己编译安装如下 前提:kafka或者redis 和 elasticsearch(版本6.8以上最好) 自行安装完毕,版本兼容已经做了,理论不用考虑ES版本 打包 maven deploy -DskipTests 上传包到自己的私服 私服地址到plumelog/pom.xml改      UTF-8      http://172.16.249.94:4000

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值