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

github 地址:honey-tracing

正文

一. 为什么不用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 = 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) {
                
            }
        }

        
        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) {
                        
                    }
                }
                throw e;
            }

            for (RestTemplateSpanDecorator restTemplateSpanDecorator : restTemplateSpanDecorators) {
                try {
                    restTemplateSpanDecorator.onResponse(request, clientHttpResponse, span);
                } catch (Exception e) {
                    
                }
            }
        } finally {
            span.finish();
            
            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) {
            
            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) {
        
        ((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 + 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) {
            
            restTemplate.getInterceptors().add(new HoneyRestTemplateTracingInterceptor(tracer, restTemplateSpanDecorators));
        }
    }

}

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

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

总结

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

相关推荐
CodeWithMe5 分钟前
【Note】《Kafka: The Definitive Guide》第四章:Kafka 消费者全面解析:如何从 Kafka 高效读取消息
分布式·kafka
Gauss松鼠会3 小时前
GaussDB应用场景全景解析:从金融核心到物联网的分布式数据库实践
数据库·分布式·物联网·金融·database·gaussdb
@Jackasher6 小时前
Redisson是如何实现分布式锁的?
分布式
❀always❀12 小时前
深入浅出分布式限流(更新中)
分布式·wpf
Bug退退退12315 小时前
RabbitMQ 幂等性
分布式·rabbitmq
{⌐■_■}1 天前
【Kafka】登录日志处理的三次阶梯式优化实践:从同步写入到Kafka多分区批处理
数据库·分布式·mysql·kafka·go
qq_529835351 天前
RabbitMQ的消息可靠传输
分布式·rabbitmq
CodeWithMe1 天前
【Note】《Kafka: The Definitive Guide》 第九章:Kafka 管理与运维实战
运维·分布式·kafka
sql2008help1 天前
1-Kafka介绍及常见应用场景
分布式·kafka
何苏三月1 天前
SpringCloud系列 - Seata 分布式事务(六)
分布式·spring·spring cloud