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

579 阅读5分钟

前言

本文将对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

github地址:honey-tracing

正文

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

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

@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的改造后的代码。

/**
 * 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,实现如下。

/**
 * 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中,如下所示。

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做了一点小小的修改,如下所示。

/**
 * 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增加内容如下。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

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

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

请求发起前:

  1. 记录请求的下游的host

接收响应后:

  1. 记录响应码。

执行异常时:

  1. 记录响应码。

装饰器实现如下。

/**
 * {@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如下所示。

/**
 * 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的拦截器,如下所示。

/**
 * 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包的结构,如下所示。

Starter包工程结构图

总结

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