SpringBoot集成LogBack实现链路追踪

上文介绍了SpringBoot集成LogBack的例子,文章末尾提到了如何区分每次请求的日志?本文介绍一下如何通过LogBack的MDC(Mapped Diagnostic Context)实现轻量级的链路追踪,直接上干货。

在日志上下文MDC创建请求ID

1、先写个工具类生成ID

public class TraceIdUtil {
    // 注意这里是变量
    public static final String TRACE_ID = "reqId";
    //获取ID
    public static String getTraceId() {
        String traceId = (String) MDC.get(TRACE_ID);
        return traceId == null ? "" : traceId;
    }
    //设置ID
    public static void setTraceId(String traceId) {
        MDC.put(TRACE_ID, traceId);
    }
    //移除ID
    public static void remove() {
        MDC.remove(TRACE_ID);
    }
    //生成ID,先随意生成一个uuid,这里可以用应用,IP,时间等生成,可以记录更多信息
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

}

2、实现HandlerInterceptor,拦截请求并设置ID,如果请求上下文已经有ID则继承过来,

记得请求之后要清空上下文的ID;


@Component
public class TraceInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
        if (!StringUtils.hasLength(traceId)) {
            TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
        } else {
            TraceIdUtil.setTraceId(traceId);
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        //请求完成,移除id
        TraceIdUtil.remove();
    }
}

3、将拦截器TraceInterceptor,注册到应用中,不然不生效。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    private Logger logger = LoggerFactory.getLogger(InterceptorConfig.class);
    //通过引入数组,将所有的拦截器实现都注册到应用,逐个addInterceptor也是可以的
    @Resource
    private List<HandlerInterceptor> handlerInterceptorList;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        WebMvcConfigurer.super.addInterceptors(registry);
        for (HandlerInterceptor handlerInterceptor : handlerInterceptorList) {
            logger.info("interceptor class={}",handlerInterceptor.getClass());
            registry.addInterceptor(handlerInterceptor);
        }
    }
}

配置文件实现打印变量

第一修改日志配置文件,打印MDC里面的变量如下,

注意看 [%X{reqId}] 这是将上下文里面的reqId变量打印出来。和上面的工具类里面的变量一致


<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern> %d{HH:mm:ss.SSS} [%thread] %-5level [%X{reqId}] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <logger name="log.config" level="INFO"/>

    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

查看日志

空行分隔开的两次请求,reqId分别是
38fc74f8ed174943a19e97d692f3cf8d,
62298a4482d94f4ebfdd0102313fb63f;

22:49:49.900 [http-nio-9090-exec-2] INFO [38fc74f8ed174943a19e97d692f3cf8d] net.img.HelloController - getuser controller
22:49:49.900 [http-nio-9090-exec-2] INFO [38fc74f8ed174943a19e97d692f3cf8d] net.img.biz.HelloBiz - getuser biz
22:49:49.936 [ForkJoinPool.commonPool-worker-9] INFO [] net.img.biz.HelloBiz - async log

22:49:58.350 [http-nio-9090-exec-3] INFO [62298a4482d94f4ebfdd0102313fb63f] net.img.HelloController - getuser controller
22:49:58.351 [http-nio-9090-exec-3] INFO [62298a4482d94f4ebfdd0102313fb63f] net.img.biz.HelloBiz - getuser biz
22:49:58.351 [ForkJoinPool.commonPool-worker-9] INFO [] net.img.biz.HelloBiz - async log

疑问

大家看到有一行日志async log,没有输出reqId,为什么?由于这行是重启了一个线程导致的上下文丢失,看代码


@Component
public class HelloBiz {
    private Logger logger = LoggerFactory.getLogger(HelloBiz.class);
    @Resource
    private SysUserMapper sysUserMapper;

    public SysUserEntity getUser(Long userId) {
        logger.info("getuser biz");
        CompletableFuture.runAsync(() -> {
            logger.info("async log");
        });
        return sysUserMapper.selectByPrimaryKey(userId);
    }
}

如何解决呢?将reqId传到新线程的上下文中如下


@Component
public class HelloBiz {
    private Logger logger = LoggerFactory.getLogger(HelloBiz.class);
    @Resource
    private SysUserMapper sysUserMapper;

    public SysUserEntity getUser(Long userId) {
        logger.info("getuser biz");
        //取出来reqId,在线程执行前put到MDC
        String traceId = TraceIdUtil.getTraceId();
        CompletableFuture.runAsync(() -> {
            MDC.put(TraceIdUtil.TRACE_ID, traceId);
            logger.info("async log");
            MDC.clear();
        });
        return sysUserMapper.selectByPrimaryKey(userId);
    }
}

新的日志如下

22:58:20.575 [http-nio-9090-exec-2] INFO [6068838d07f74f4e84b8dcc8ac88f951] net.img.HelloController - getuser controller
22:58:20.575 [http-nio-9090-exec-2] INFO [6068838d07f74f4e84b8dcc8ac88f951] net.img.biz.HelloBiz - getuser biz
22:58:20.680 [ForkJoinPool.commonPool-worker-9] INFO [6068838d07f74f4e84b8dcc8ac88f951] net.img.biz.HelloBiz - async log
22:58:28.716 [http-nio-9090-exec-3] INFO [5057bd3e4c3b4fb884578afbe284185e] net.img.HelloController - getuser controller
22:58:28.716 [http-nio-9090-exec-3] INFO [5057bd3e4c3b4fb884578afbe284185e] net.img.biz.HelloBiz - getuser biz
22:58:28.716 [ForkJoinPool.commonPool-worker-9] INFO [5057bd3e4c3b4fb884578afbe284185e] net.img.biz.HelloBiz - async log 

更优雅一点可以自己包装一下多线程或线程池;给个简单例子

包装类。


public class FutureProxy<T> {
    public  Void  run(Runnable runnable) {
        String traceId = TraceIdUtil.getTraceId();
        CompletableFuture.runAsync(() -> {
            MDC.put(TraceIdUtil.TRACE_ID, traceId);
            runnable.run();
            MDC.clear();
        });
        return null;
    }
}

改一下上面的代码,效果一样

@Component
public class HelloBiz {
    private Logger logger = LoggerFactory.getLogger(HelloBiz.class);
    @Resource
    private SysUserMapper sysUserMapper;

    public SysUserEntity getUser(Long userId) {
        logger.info("getuser biz");
        new FutureProxy<Void>().run(() -> logger.info("async log"));
        return sysUserMapper.selectByPrimaryKey(userId);
    }
}
© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
永远开心nice的头像 - 鹿快
评论 共4条

请登录后发表评论