前言
本文将对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 写法调用到Scope 的close() 方法后就直接结束了,没有任何扩展点可以在Scope 的close() 方法之后执行,这就导致我们无法将调用下游的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;
}
}
相较于改造之前,增强了如下两点。
- 使用了装饰器。在RestTemplate 发起请前,收到响应后和发生异常时,对Span 进行了增强,本质就是记录一些字段信息到Span中;
- 记录了requestStacks 。RestTemplate 在发起请求时,会创建一个代表下游的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拦截器的装饰器,装饰器需要做如下的事情。
请求发起前:
- 记录请求的下游的host。
接收响应后:
- 记录响应码。
执行异常时:
- 记录响应码。
装饰器实现如下。
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());
}
}
本质就是往Span 的tag 里面以键值对的形式添加我们想要记录的信息,其中使用到的解析域名的工具类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的分布式链路追踪拦截器的实现进行了说明,并分析了如何提供装饰器进行功能扩展与增强。