欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot项目中通过MDC和自定义Filter操作traceId实现日志链路追踪

程序员文章站 2022-07-03 15:40:21
...

1.背景简述

  • 依赖原始的log4j2配置,很难从某服务庞杂的日志中,单独找寻出某次API调用的全部日志。
  • 本文通过在日志中打印唯一的traceId来实现每次调用的追踪。

2.关键思路

2.1.MDC

  • 日志追踪目标是每次请求级别的,也就是说同一个接口的每次请求,都应该有不同的traceId。
  • 每次接口请求,都是一个单独的线程,所以自然我们很容易考虑到通过ThreadLocal实现上述需求。
  • 考虑到log4j本身已经提供了类似的功能MDC,所以直接使用MDC进行实现。
  • 关于MDC的简述
    • Mapped Diagnostic Context,即:映射诊断环境。
    • MDC是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。
    • MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。
  • 关于MDC的关键操作
    • 向MDC中设置值:MDC.put(key, value);
    • 从MDC中取值:MDC.get(key);
    • 将MDC中内容打印到日志中:%X{key}

2.2.自定义Filter

  • 假定已经解决了traceId的存放问题,那么何时进行traceId的存放呢?其实有多重实现思路,例如:过滤器、AOP、拦截器等等。
  • 本文采用过滤器的形式,即:自定义一个Filter,继承自GenericFilterBean
  • 其他实现方式可自行探索。

3.实现步骤

3.1.原始示例

1.定义一个简单的接口:

/**
 * <p></P>
 *
 * @author hanchao
 */
@RestController
public class DemoController {
    private final Logger logger = Logger.getLogger(DemoController.class);

    @GetMapping("/demo/by-name")
    public String demo(String name) {
        logger.info("name:" + name);
        return name;
    }
}

2.在浏览器调用接口: http://localhost:8080/demo/by-name?name=zhangsan

3.查看相关日志:

 INFO pers.hanchao.trace.controller.DemoController:19 - name:zhangsan 

3.2.TraceId操作工具类

增加TraceId操作的工具类,提供traceId的默认取值、setter、getter和生成。

/**
 * <p>traceId工具类</P>
 *
 * @author hanchao
 */
public class TraceIdUtil {
    private static final String TRACE_ID = "traceId";
    /**
     * 当traceId为空时,显示的traceId。随意。
     */
    private static final String DEFAULT_TRACE_ID = "0";

    /**
     * 设置traceId
     */
    public static void setTraceId(String traceId) {
        //如果参数为空,则设置默认traceId
        traceId = StringUtils.isBlank(traceId) ? DEFAULT_TRACE_ID : traceId;
        //将traceId放到MDC中
        MDC.put(TRACE_ID, traceId);
    }

    /**
     * 获取traceId
     */
    public static String getTraceId() {
        //获取
        String traceId = MDC.get(TRACE_ID);
        //如果traceId为空,则返回默认值
        return StringUtils.isBlank(traceId) ? DEFAULT_TRACE_ID : traceId;
    }

    /**
     * 判断traceId为默认值
     */
    public static Boolean defaultTraceId(String traceId) {
        return DEFAULT_TRACE_ID.equals(traceId);
    }

    /**
     * 生成traceId
     */
    public static String genTraceId() {
        return UUID.randomUUID().toString();
    }
}

3.3.自定义TraceId过滤器

自定义过滤器,对全部请求进行traceId处理。这里处理有些粗暴,可自行细化。

/**
 * <p>traceId过滤器,用于设置traceId</P>
 *
 * @author hanchao
 */
@WebFilter(urlPatterns = "/*", filterName = "traceIdFilter")
@Order(1)
public class TraceIdFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //traceId初始化
        initTraceId((HttpServletRequest) servletRequest);
        //执行后续过滤器
        filterChain.doFilter(servletRequest,servletResponse);
    }

    /**
     * traceId初始化
     */
    private void initTraceId(HttpServletRequest request) {
        //尝试获取http请求中的traceId
        String traceId = request.getParameter("traceId");

        //如果当前traceId为空或者为默认traceId,则生成新的traceId
        if (StringUtils.isBlank(traceId) || TraceIdUtil.defaultTraceId(traceId)){
            traceId = TraceIdUtil.genTraceId();
        }

        //设置traceId
        TraceIdUtil.setTraceId(traceId);
    }
}

3.4.启用自定义过滤器

不要忘记在SpringBoot的启动类加上@ServletComponentScan注解,否则自定义的Filter无法生效。

/**
 * 使用嵌入式容器时,可以使用@ServletComponentScan启用@WebServlet,@ WebFilter和@WebListener注释类的自动注册。
 */
@ServletComponentScan(basePackages = "pers.hanchao.trace.filter")
@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

3.5.修改log4j2的layout格式

修改日志的layout格式,将MDC中的traceId打印出来:

<!-- 原始格式 -->
<PatternLayout pattern="%5p %c:%L - %m %throwable{separator( --> )}%n"/>

<!-- 增加traceId的格式 -->
<PatternLayout pattern="%5p traceId:%X{traceId} %c:%L - %m %throwable{separator( --> )}%n"/>

3.6.受影响的示例

1.在浏览器多次调用接口: http://localhost:8080/demo/by-name?name=zhangsan

2.查看相关日志:

 INFO traceId:5ee05f9b-432c-401f-ae24-6adaf2f31cf4 pers.hanchao.trace.controller.DemoController:19 - name:zhangsan 
 INFO traceId:b835352f-3a22-462c-965e-c426309ae3b8 pers.hanchao.trace.controller.DemoController:19 - name:zhangsan 
 INFO traceId:942d4f8f-f3c6-4688-a534-3429b6c9e92d pers.hanchao.trace.controller.DemoController:19 - name:zhangsan 

4.更多思考

  • 如果一个业务操作,需要进行多个服务的多个接口相互调用,则如何传递traceId?