Spring Boot项目如何实现分布式日志链路追踪

1.概述
作为一名后端开发工程师,排查系统问题用得最多的手段之一就是查看系统日志,在当下主要的分布式集群环境中一般使用ELK(Elasticsearch , Logstash, Kibana)来统一收集日志,以便后续查看日志定位追踪相关问题。但是在并发情况下,大量的系统用户即多线程并发访问后端服务导致同一个请求的日志记录不再是连续相邻的,此时多个请求的日志是一起串行输出到文件中,所以我们筛选出指定请求的全部相关日志还是比较麻烦的,同时当后端异步处理功能逻辑以及微服务的下游服务调用日志追踪也有着相同的问题。
为了快速排查、定位、解决日常反馈的系统问题,我们就必须解决上面所说的查看请求日志的痛点。解决方案就是:每个请求都使用一个唯一标识traceId来追踪全部的链路显示在日志中,并且不修改原有的打印方式(代码无入侵),然后使用使用Logback的MDC机制日志模板中加入traceId标识,取值方式为%X{traceId} 。这样在收集的日志文件中就可以看到每行日志有一个tracceId值,每个请求的值都不一样,这样我们就可以根据traceId查询过滤出一次请求的所有上下文日志了。

2.实现方案
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从MDC 中获取所需的信息即可。MDC的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
由于MDC内部使用的是ThreadLocal所以只有本线程才有效,子线程和下游的服务MDC里的值会丢失;所以方案主要的难点是解决traceId值的传递问题,需要重点关注一下两点:

MDC中traceId数据如何传递给下游服务,下游服务如何接收traceId并放入MDC中
异步的情况下(线程池)如何把traceId值传给子线程。

2.1 设置日志模板
无论是我们的项目使用的是log4j还是logback框架,我们都需要先调整日志配置文件的日志格式如下:

<!-- 日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="[%X{traceId}] [%-5p] [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%t@${PID}]  %c %M : %m%n"/>

这样才能有效地把traceId收集到日志文件中。
2.2 请求上下文设置traceId并有效传递下游服务
按照上面说的,每个请求使用一个唯一标识traceId来追踪一次请求的全部日志,这就要求我们的traceId必须保证唯一性,不然就会出现请求日志混乱问题,是绝对不允许的。这里我们利用hutool框架的生成id工具IdUtil来生成唯一值,可以生成uuid或者使用雪花算法Snowflake生成唯一id都可以,因为这里id是记录在日志文件中做唯一标识用的,所以对id字符类型,递增性那些没啥要求,只要唯一标识即可,按照之前习惯,我就用雪花算法生成唯一id标识了。
生成traceId并放入到MDC上下文中


public class WebTraceFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws IOException, ServletException {
        try {
            String traceId = request.getHeader(MDCTraceUtils.TRACE_ID_HEADER);
            if (StrUtil.isEmpty(traceId)) {
                MDCTraceUtils.addTrace();
            } else {
                MDCTraceUtils.putTrace(traceId);
            }
            filterChain.doFilter(request, response);
        } finally {
            MDCTraceUtils.removeTrace();
        }
    }
}

这里通过一个过滤器来设置traceId放入到MDC中,可以将该过滤器的执行优先级设置比较靠前,这样就可以有效保证我们一次请求上下文的日志中都有traceId了。同时这个过滤器我们是集成在自定义实现的web starter中,公司的所有服务都会引用web starter集成该过滤器,意味着只要我们请求下游服务时添加了traceId这个header,下游服务执行到该过滤器时就会拿到上游服务传递过来的traceId值放入到当前服务的MDC中。MDCTraceUtils工具类代码如下:

public class MDCTraceUtils {
    /**
     * 追踪id的名称
     */
    public static final String KEY_TRACE_ID = "traceId";

    /**
     * 日志链路追踪id信息头
     */
    public static final String TRACE_ID_HEADER = "x-traceId-header";


    /**
     * 创建traceId并赋值MDC
     */
    public static void addTrace() {
        String traceId = createTraceId();
        MDC.put(KEY_TRACE_ID, traceId);
    }

    /**
     * 赋值MDC
     */
    public static void putTrace(String traceId) {
        MDC.put(KEY_TRACE_ID, traceId);
    }

    /**
     * 获取MDC中的traceId值
     */
    public static String getTraceId() {
        return MDC.get(KEY_TRACE_ID);
    }

    /**
     * 清除MDC的值
     */
    public static void removeTrace() {
        MDC.remove(KEY_TRACE_ID);
    }

    /**
     * 创建traceId
     */
    public static String createTraceId() {
        return IdUtil.getSnowflake().nextIdStr();
    }

}

接下来我们就来演示下

  • 22
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奔向理想的星辰大海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值