6. 分布式链路追踪RestTemplate拦截器实现设计

前言

本文将对4. 分布式链路追踪客户端工具包Starter设计一文中的RestTemplate 的拦截器进行一个增强设计,以使得使用RestTemplate 调用下游时,可以得到3. 分布式链路追踪的链路日志设计一文中所定义的链路日志的requestStacks字段内容。

相关版本依赖如下。

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

正文

一. 为什么不用Opentracing提供的拦截器

实际上Opentracing 也提供了RestTemplate 的拦截器,叫做TracingRestTemplateInterceptor ,其intercept() 方法实现如下。

java 复制代码
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] body,
                                    ClientHttpRequestExecution execution) throws IOException {
    ClientHttpResponse httpResponse;

    Span span = tracer.buildSpan(httpRequest.getMethod().toString())
            .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
            .start();
    tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS,
            new HttpHeadersCarrier(httpRequest.getHeaders()));

    for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
        try {
            spanDecorator.onRequest(httpRequest, span);
        } catch (RuntimeException exDecorator) {
            log.error("Exception during decorating span", exDecorator);
        }
    }

    try (Scope scope = tracer.activateSpan(span)) {
        httpResponse = execution.execute(httpRequest, body);
        for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
            try {
                spanDecorator.onResponse(httpRequest, httpResponse, span);
            } catch (RuntimeException exDecorator) {
                log.error("Exception during decorating span", exDecorator);
            }
        }
    } catch (Exception ex) {
        for (RestTemplateSpanDecorator spanDecorator : spanDecorators) {
            try {
                spanDecorator.onError(httpRequest, ex, span);
            } catch (RuntimeException exDecorator) {
                log.error("Exception during decorating span", exDecorator);
            }
        }
        throw ex;
    } finally {
        span.finish();
    }

    return httpResponse;
}

其实就是在我们之前实现的RestTemplate 拦截器的基础上加入了装饰器修饰,看起来好像也能用,但是这里有一个问题,上述TracingRestTemplateInterceptor 在通过try-with 写法调用到Scopeclose() 方法后就直接结束了,没有任何扩展点可以在Scopeclose() 方法之后执行,这就导致我们无法将调用下游的Span 转换为requestStacks 并记录在当前节点的Span 中,所以TracingRestTemplateInterceptor不能直接使用。

二. RestTemplate拦截器实现设计

下面直接看一下HoneyRestTemplateTracingInterceptor的改造后的代码。

java 复制代码
/**
 * RestTemplate客户端的分布式链路追踪拦截器。
 */
public class HoneyRestTemplateTracingInterceptor implements ClientHttpRequestInterceptor {

    private final Tracer tracer;
    private final List<RestTemplateSpanDecorator> restTemplateSpanDecorators;

    public HoneyRestTemplateTracingInterceptor(Tracer tracer, List<RestTemplateSpanDecorator> restTemplateSpanDecorators) {
        this.tracer = tracer;
        this.restTemplateSpanDecorators = restTemplateSpanDecorators;
    }

    @NotNull
    public ClientHttpResponse intercept(@NotNull HttpRequest request, @NotNull byte[] body,
                                        @NotNull ClientHttpRequestExecution execution) throws IOException {
        JaegerSpan parentSpan = (JaegerSpan) tracer.activeSpan();
        if (shouldIgnore(parentSpan)) {
            return execution.execute(request, body);
        }

        ClientHttpResponse clientHttpResponse;
        // 创建代表下游的Span并启动
        Span span = tracer.buildSpan(HONEY_REST_TEMPLATE_NAME)
                .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
                .start();

        tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(request.getHeaders()));

        for (RestTemplateSpanDecorator restTemplateSpanDecorator : restTemplateSpanDecorators) {
            try {
                restTemplateSpanDecorator.onRequest(request, span);
            } catch (Exception e) {
                // do nothing
            }
        }

        // 激活代表下游的Span
        try (Scope scope = tracer.activateSpan(span)) {
            try {
                clientHttpResponse = execution.execute(request, body);
            } catch (Exception e) {
                for (RestTemplateSpanDecorator restTemplateSpanDecorator : restTemplateSpanDecorators) {
                    try {
                        restTemplateSpanDecorator.onError(request, e, span);
                    } catch (Exception onErrorEx) {
                        // do nothing
                    }
                }
                throw e;
            }

            for (RestTemplateSpanDecorator restTemplateSpanDecorator : restTemplateSpanDecorators) {
                try {
                    restTemplateSpanDecorator.onResponse(request, clientHttpResponse, span);
                } catch (Exception e) {
                    // do nothing
                }
            }
        } finally {
            span.finish();
            // 将代表下游的Span作为requestStack记录在parentSpan中
            tracer.activeSpan().log(RequestStackUtil.assembleRequestStack((JaegerSpan) span));
        }

        return clientHttpResponse;
    }

    private boolean shouldIgnore(JaegerSpan activeSpan) {
        return activeSpan == null;
    }

}

相较于改造之前,增强了如下两点。

  1. 使用了装饰器。在RestTemplate 发起请前,收到响应后和发生异常时,对Span 进行了增强,本质就是记录一些字段信息到Span中;
  2. 记录了requestStacksRestTemplate 在发起请求时,会创建一个代表下游的Span 并启动和激活,当请求执行完毕后,这个Span 中就有请求下游的各种信息,我们将这些信息作为requestStacks 记录在了当前节点的Span中。

创建requestStacks 的工具类RequestStackUtil,实现如下。

java 复制代码
/**
 * requestStack记录工具类。
 */
public class RequestStackUtil {

    /**
     * 生成使用HTTP方式访问下游的requestStack。
     */
    public static Map<String, Object> assembleRequestStack(JaegerSpan span) {
        Map<String, Object> requestStack = new HashMap<>();
        requestStack.put(LOG_EVENT_KIND, LOG_EVENT_KIND_REQUEST_STACK);
        requestStack.put(FIELD_SUB_SPAN_ID, span.context().toSpanId());
        requestStack.put(FIELD_SUB_HTTP_CODE, span.getTags().get(FIELD_HTTP_CODE));
        requestStack.put(FIELD_SUB_TIMESTAMP, span.getStart());
        requestStack.put(FIELD_SUB_DURATION, span.getDuration());
        requestStack.put(FIELD_SUB_HOST, span.getTags().get(FIELD_HOST));
        return requestStack;
    }

}

使用到的常量在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";
    public static final String FIELD_SUB_SPAN_ID = "subSpanId";
    public static final String FIELD_SUB_HTTP_CODE = "subHttpCode";
    public static final String FIELD_SUB_TIMESTAMP = "subTimestamp";
    public static final String FIELD_SUB_DURATION = "subDuration";
    public static final String FIELD_SUB_HOST = "subHost";

    public static final String HOST_PATTERN_STR = "(?<=(https://|http://)).*?(?=/)";

    public static final String SLASH = "/";

    public static final String LOG_EVENT_KIND = "logEventKind";
    public static final String LOG_EVENT_KIND_REQUEST_STACK = "requestStack";

}

然后是为了通过编译,对HoneyRestTemplateTracingConfig做了一点小小的修改,如下所示。

java 复制代码
/**
 * RestTemplate分布式链路追踪配置类。
 */
@ConditionalOnBean(RestTemplate.class)
@Configuration
@AutoConfigureAfter(HoneyTracingConfig.class)
public class HoneyRestTemplateTracingConfig {

    public HoneyRestTemplateTracingConfig(List<RestTemplate> restTemplates, Tracer tracer) {
        for (RestTemplate restTemplate : restTemplates) {
            // todo 还要判断RestTemplate里是否已经添加了HoneyRestTemplateTracingInterceptor
            restTemplate.getInterceptors().add(new HoneyRestTemplateTracingInterceptor(tracer, new ArrayList<>()));
        }
    }

}

最后增加了commons-lang3 的依赖,pom增加内容如下。

xml 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

三. RestTemplate拦截器的装饰器实现设计

最后就是要设计RestTemplate拦截器的装饰器,装饰器需要做如下的事情。

请求发起前:

  1. 记录请求的下游的host

接收响应后:

  1. 记录响应码。

执行异常时:

  1. 记录响应码。

装饰器实现如下。

java 复制代码
/**
 * {@link RestTemplate}的{@link Span}装饰器。
 */
public class HoneyRestTemplateSpanDecorator implements RestTemplateSpanDecorator {

    @Override
    public void onRequest(HttpRequest request, Span span) {
        ((JaegerSpan) span).setTag(FIELD_HOST, UrlUtil.getHostFromUri(request.getURI().toString()));
    }

    @Override
    public void onResponse(HttpRequest request, ClientHttpResponse response, Span span) {
        try {
            ((JaegerSpan) span).setTag(FIELD_HTTP_CODE, response.getRawStatusCode());
        } catch (Exception e) {
            ((JaegerSpan) span).setTag(FIELD_HTTP_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
    }

    @Override
    public void onError(HttpRequest request, Throwable ex, Span span) {
        // todo 调用下游失败时设置500好像有点不合理
        ((JaegerSpan) span).setTag(FIELD_HTTP_CODE, HttpStatus.INTERNAL_SERVER_ERROR.value());
    }

}

本质就是往Spantag 里面以键值对的形式添加我们想要记录的信息,其中使用到的解析域名的工具类UrlUtil如下所示。

java 复制代码
/**
 * Url处理工具类。
 */
public class UrlUtil {

    private static final Pattern HOST_PATTERN = Pattern.compile(HOST_PATTERN_STR);

    /**
     * 从请求URI中解析出域名。<br/>
     * http://www.baidu.com/<br/>
     * http://www.baidu.com<br/>
     * https://www.baidu.com/<br/>
     * https://www.baidu.com<br/>
     */
    public static String getHostFromUri(String uri) {
        if (!uri.endsWith(SLASH)) {
            // 如果uri不以/结尾则需要手动添加上
            // 否则正则匹配会无法将域名匹配出来
            uri = uri + SLASH;
        }
        if (StringUtils.isNotEmpty(uri)) {
            Matcher matcher = HOST_PATTERN.matcher(uri);
            if (matcher.find()) {
                return matcher.group(0);
            }
        }
        return StringUtils.EMPTY;
    }

}

最后我们还需要修改一下HoneyRestTemplateTracingConfig ,将装饰器给到RestTemplate的拦截器,如下所示。

java 复制代码
/**
 * RestTemplate分布式链路追踪配置类。
 */
@ConditionalOnBean(RestTemplate.class)
@Configuration
@AutoConfigureAfter(HoneyTracingConfig.class)
@Import(HoneyRestTemplateSpanDecorator.class)
public class HoneyRestTemplateTracingConfig {

    public HoneyRestTemplateTracingConfig(List<RestTemplate> restTemplates, Tracer tracer,
                                          List<RestTemplateSpanDecorator> restTemplateSpanDecorators) {
        for (RestTemplate restTemplate : restTemplates) {
            // todo 还要判断RestTemplate里是否已经添加了HoneyRestTemplateTracingInterceptor
            restTemplate.getInterceptors().add(new HoneyRestTemplateTracingInterceptor(tracer, restTemplateSpanDecorators));
        }
    }

}

至此,RestTemplate拦截器的装饰器实现设计分析完毕。

再来看一下现在Starter包的结构,如下所示。

总结

本文对RestTemplate的分布式链路追踪拦截器的实现进行了说明,并分析了如何提供装饰器进行功能扩展与增强。

相关推荐
rzl022 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习8 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl21 分钟前
深度解读jdk8 HashMap设计与源码
java
Falling4225 分钟前
使用 CNB 构建并部署maven项目
后端
guojl27 分钟前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假35 分钟前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文43 分钟前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端
A~taoker1 小时前
taoker的项目维护(ng服务器)
java·开发语言
萧曵 丶1 小时前
Rust 中的返回类型
开发语言·后端·rust