5. 分布式链路追踪TracingFilter改造增强设计

前言

4. 分布式链路追踪客户端工具包Starter设计一文中,我们实现了基础的Starter 包,里面提供了我们自己定义的Servlet 过滤器和RestTemplate 拦截器,其中Servlet 过滤器叫做HoneyTracingFilter ,仅提供了提取SpanContext ,创建Span 和开启Span 的基础功能,所以本文将围绕如何增强Servlet过滤器展开讨论。

相关版本依赖如下。

opentracing-api 版本:0.33.0
opentracing-spring-web 版本:4.1.0
jaeger-client 版本:1.8.1
Springboot 版本:2.7.6

正文

一. Opentracing提供的TracingFilter

其实最简单的增强方式,就是使用TracingFilter 来替换我们自己实现的HoneyTracingFilter ,下面给出TracingFilter的源码实现。

java 复制代码
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
        throws IOException, ServletException {

    HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
    HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

    // 基于正则来判断当前请求URI是否不需要记录链路信息
    if (!isTraced(httpRequest, httpResponse)) {
        chain.doFilter(httpRequest, httpResponse);
        return;
    }

    if (servletRequest.getAttribute(SERVER_SPAN_CONTEXT) != null) {
        chain.doFilter(servletRequest, servletResponse);
    } else {
        // 使用Extractor从HTTP请求头中提取出SpanContext
        SpanContext extractedContext = tracer.extract(Format.Builtin.HTTP_HEADERS,
                new HttpServletRequestExtractAdapter(httpRequest));

        // 创建Span并将其作为提取出来的Span的子Span
        final Span span = tracer.buildSpan(httpRequest.getMethod())
                .asChildOf(extractedContext)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
                .start();

        httpRequest.setAttribute(SERVER_SPAN_CONTEXT, span.context());

        // 在请求的一开始使用装饰器来装饰Span
        // 这里的装饰器是很重要的扩展点
        for (ServletFilterSpanDecorator spanDecorator: spanDecorators) {
            spanDecorator.onRequest(httpRequest, span);
        }

        // 将创建出来的Span激活
        try (Scope scope = tracer.activateSpan(span)) {
            chain.doFilter(servletRequest, servletResponse);
            if (!httpRequest.isAsyncStarted()) {
                for (ServletFilterSpanDecorator spanDecorator : spanDecorators) {
                    spanDecorator.onResponse(httpRequest, httpResponse, span);
                }
            }
        } catch (Throwable ex) {
            // 在请求异常时使用装饰器来装饰Span
            for (ServletFilterSpanDecorator spanDecorator : spanDecorators) {
                spanDecorator.onError(httpRequest, httpResponse, ex, span);
            }
            throw ex;
        } finally {
            if (httpRequest.isAsyncStarted()) {
                // 异步Servlet场景下添加监听器
                httpRequest.getAsyncContext()
                        .addListener(new AsyncListener() {
                            @Override
                            public void onComplete(AsyncEvent event) throws IOException {
                                HttpServletRequest httpRequest = (HttpServletRequest) event.getSuppliedRequest();
                                HttpServletResponse httpResponse = (HttpServletResponse) event.getSuppliedResponse();
                                // 异步操作完成时使用装饰器装饰Span
                                for (ServletFilterSpanDecorator spanDecorator: spanDecorators) {
                                    spanDecorator.onResponse(httpRequest,
                                            httpResponse,
                                            span);
                                }
                                span.finish();
                            }

                            @Override
                            public void onTimeout(AsyncEvent event) throws IOException {
                                HttpServletRequest httpRequest = (HttpServletRequest) event.getSuppliedRequest();
                                HttpServletResponse httpResponse = (HttpServletResponse) event.getSuppliedResponse();
                                // 异步操作超时时使用装饰器装饰Span
                                for (ServletFilterSpanDecorator spanDecorator : spanDecorators) {
                                    spanDecorator.onTimeout(httpRequest,
                                            httpResponse,
                                            event.getAsyncContext().getTimeout(),
                                            span);
                                }
                            }

                            @Override
                            public void onError(AsyncEvent event) throws IOException {
                                HttpServletRequest httpRequest = (HttpServletRequest) event.getSuppliedRequest();
                                HttpServletResponse httpResponse = (HttpServletResponse) event.getSuppliedResponse();
                                // 异步操作异常时使用装饰器装饰Span
                                for (ServletFilterSpanDecorator spanDecorator: spanDecorators) {
                                    spanDecorator.onError(httpRequest,
                                            httpResponse,
                                            event.getThrowable(),
                                            span);
                                }
                            }

                            @Override
                            public void onStartAsync(AsyncEvent event) throws IOException {
                            }
                        });
            } else {
                // 请求完成时设置完成时间并记录Span
                span.finish();
            }
        }
    }
}

通过阅读TracingFilter源码,我们可以得到如下几种扩展增强。

  1. Servlet 自身的urlPatterns 机制。可以通过配置urlPatterns,决定哪些请求需要打印链路信息;
  2. TracingFilterskipPattern 机制。可以通过配置skipPattern,决定哪些请求不需要打印链路信息;
  3. 装饰器ServletFilterSpanDecorator 。可以提供ServletFilterSpanDecorator 给到TracingFilter,这样在收到请求,返回响应和处理异常时均可以做一些扩展操作;

二. urlPatterns和skipPattern设计

在第一节中得到的TracingFilter 的几种增强,其中第1和第2点的urlPatternsskipPattern,可以提供出来供用户配置,本节对这部分进行实现。

首先是配置属性类里面需要加入urlPatternsskipPattern的配置属性,如下所示。

java 复制代码
/**
 * 分布式链路追踪配置属性类。
 */
@ConfigurationProperties("honey.tracing")
public class HoneyTracingProperties {

    private boolean enabled;

    private HttpUrlProperties httpUrl = new HttpUrlProperties();

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public HttpUrlProperties getHttpUrl() {
        return httpUrl;
    }

    public void setHttpUrl(HttpUrlProperties httpUrl) {
        this.httpUrl = httpUrl;
    }

    public static class HttpUrlProperties {
        /**
         * 按照/url1,/url2这样配置。
         */
        private String urlPattern = "/*";
        /**
         * 按照/url1|/honey.*这样配置。
         */
        private String skipPattern = "";

        public String getUrlPattern() {
            return urlPattern;
        }

        public void setUrlPattern(String urlPattern) {
            this.urlPattern = urlPattern;
        }

        public String getSkipPattern() {
            return skipPattern;
        }

        public void setSkipPattern(String skipPattern) {
            this.skipPattern = skipPattern;
        }
    }

}

然后注册过滤器的配置类HoneyTracingFilterConfig需要做如下修改。

java 复制代码
/**
 * Servlet过滤器配置类。
 */
@Configuration
@AutoConfigureAfter(HoneyTracingConfig.class)
public class HoneyTracingFilterConfig {

    @Autowired
    private HoneyTracingProperties honeyTracingProperties;

    @Bean
    public FilterRegistrationBean<TracingFilter> honeyTracingFilter(Tracer tracer) {
        String urlPattern = honeyTracingProperties.getHttpUrl().getUrlPattern();
        String skipPatternStr = honeyTracingProperties.getHttpUrl().getSkipPattern();
        Pattern skipPattern = Pattern.compile(skipPatternStr);

        FilterRegistrationBean<TracingFilter> registrationBean = new FilterRegistrationBean<>();
        // 为TracingFilter添加注册时的urlPattern
        registrationBean.addUrlPatterns(urlPattern);
        // TracingFilter的优先级要尽可能的高
        registrationBean.setOrder(Integer.MIN_VALUE);
        // 创建TracingFilter时指定装饰器集合和skipPattern
        // 这里暂时传入空的装饰器集合
        registrationBean.setFilter(new TracingFilter(tracer, new ArrayList<>(), skipPattern));

        return registrationBean;
    }

}

三. TracingFilter的装饰器设计

通过为TracingFilter 注册ServletFilterSpanDecorator 装饰器,可以让我们在收到请求,返回响应和处理异常时做一些扩展操作,例如记录请求urlapi 和返回码等,下面实现一个装饰器HoneyServletFilterSpanDecorator,其提供如下几个功能。

收到请求时记录:

  1. 请求的host
  2. 请求的api

返回响应时记录:

  1. 响应码。

处理异常时记录:

  1. 响应码。

实现如下。

java 复制代码
/**
 * {@link TracingFilter}的装饰器。
 */
public class HoneyServletFilterSpanDecorator implements ServletFilterSpanDecorator {

    @Override
    public void onRequest(HttpServletRequest httpServletRequest, Span span) {
        span.setTag(FIELD_HOST, getHostFromRequest(httpServletRequest));
        span.setTag(FIELD_API, httpServletRequest.getRequestURI());
    }

    @Override
    public void onResponse(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Span span) {
        span.setTag(FIELD_HTTP_CODE, httpServletResponse.getStatus());
    }

    @Override
    public void onError(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Throwable exception, Span span) {
        span.setTag(FIELD_HTTP_CODE, httpServletResponse.getStatus());
    }

    @Override
    public void onTimeout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, long timeout, Span span) {
        // todo
    }

    private String getHostFromRequest(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getScheme()
                + "://"
                + httpServletRequest.getServerName()
                + ":"
                + httpServletRequest.getServerPort();
    }

}

相关的常量字段记录在CommonConstants中,如下所示。

java 复制代码
public class CommonConstants {

    public static final double DEFAULT_SAMPLE_RATE = 1.0;

    public static final String HONEY_TRACER_NAME = "HoneyTracer";
    public static final String HONEY_REST_TEMPLATE_NAME = "HoneyRestTemplate";

    public static final String FIELD_HOST = "host";
    public static final String FIELD_API = "api";
    public static final String FIELD_HTTP_CODE = "httpCode";

}

在注册TracingFilter 时需要将HoneyServletFilterSpanDecorator 设置给TracingFilter ,对应的配置类HoneyTracingFilterConfig修改如下。

java 复制代码
/**
 * Servlet过滤器配置类。
 */
@Configuration
@AutoConfigureAfter(HoneyTracingConfig.class)
public class HoneyTracingFilterConfig {

    @Autowired
    private HoneyTracingProperties honeyTracingProperties;

    @Bean
    public FilterRegistrationBean<TracingFilter> honeyTracingFilter(Tracer tracer,
                                                                    List<ServletFilterSpanDecorator> decorators) {
        String urlPattern = honeyTracingProperties.getHttpUrl().getUrlPattern();
        String skipPatternStr = honeyTracingProperties.getHttpUrl().getSkipPattern();
        Pattern skipPattern = Pattern.compile(skipPatternStr);

        FilterRegistrationBean<TracingFilter> registrationBean = new FilterRegistrationBean<>();
        // 为TracingFilter添加注册时的urlPattern
        registrationBean.addUrlPatterns(urlPattern);
        // TracingFilter的优先级要尽可能的高
        registrationBean.setOrder(Integer.MIN_VALUE);
        // 创建TracingFilter时指定装饰器集合和skipPattern
        // 这里暂时传入空的装饰器集合
        registrationBean.setFilter(new TracingFilter(tracer, decorators, skipPattern));

        return registrationBean;
    }

    @Bean
    public ServletFilterSpanDecorator honeyServletFilterSpanDecorator() {
        return new HoneyServletFilterSpanDecorator();
    }

}

至此,我们就使用装饰器装饰了TracingFilter ,效果就是最终在TracingFilter 调用到Spanfinish() 方法时,我们可以从Spantags 中拿到本次请求的hostapihttpCode,这些数据可以最终在打印链路日志时使用。

最后给出工程目录结构图。

总结

本文在4. 分布式链路追踪客户端工具包Starter设计的基础上,使用TracingFilter 替换了我们自己实现的HoneyTracingFilter ,并且基于urlPatternsskipPattern和装饰器进行了扩展增强。

相关推荐
小游鱼KF1 分钟前
Spring学习前置知识
java·学习·spring
扎克begod5 分钟前
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
java·开发语言·python
青灯文案16 分钟前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
我就是程序猿16 分钟前
tomcat的配置
java·tomcat
阳光阿盖尔22 分钟前
EasyExcel的基本使用——Java导入Excel数据
java·开发语言·excel
二十雨辰24 分钟前
[苍穹外卖]-12Apache POI入门与实战
java·spring boot·mybatis
程序员皮皮林24 分钟前
开源PDF工具 Apache PDFBox 认识及使用(知识点+案例)
java·pdf·开源·apache
蔚一25 分钟前
Java设计模式—面向对象设计原则(三) -----> 依赖倒转原则DIP(完整详解,附有代码+案例)
java·开发语言·设计模式·intellij-idea·依赖倒置原则
liang899930 分钟前
SpringSecurity原理解析(七):权限校验流程
java·开发语言
搁浅°87939 分钟前
IO文件拷贝
java·开发语言