54. 自定义

感谢SpanInjectorSpanExtractor,您可以自定义 spans 的创建和传播方式。

目前有两种 built-in 方法在进程之间传递跟踪信息:

  • 通过 Spring Integration

  • 通过 HTTP

Span ID 从 Zipkin-compatible(B3)headers(Message或 HTTP headers)中提取,以启动或加入现有跟踪。跟踪信息被注入到任何出站请求中,因此下一跳可以提取它们。

编码跟踪 context 的默认方式是通过包含traceId-spanId-sampled表示法(e.g. 0000000000000005-0000000000000004-1)的b3标头完成的。为了向后兼容,如果b3标头不存在,我们还检查是否存在X-B3条目,并从那里检索跟踪 context e.g. (X-B3-TraceId: 0000000000000005X-B3-SpanId: 0000000000000004X-B3-Sampled: 1)。

与之前版本的 Sleuth 相比,key 的变化是 Sleuth 正在实现 Open Tracing 的TextMap概念。在 Sleuth 中,它被称为SpanTextMap。基本上 idea 是任何通信方式(e.g. 消息,http 请求,etc.)都可以通过SpanTextMap抽象出来。这个抽象定义了如何将数据插入到载体中以及如何从那里检索它。如果你想要的话,感谢这个要检测一个新的 HTTP library,它使用FooRequest作为发送 HTTP 请求的意思,那么你必须创建一个SpanTextMap的_imple 实现,它在检索和插入 HTTP headers 方面将 calls 委托给FooRequest

54.1 Spring Integration

对于 Spring Integration,有 2 个接口负责从Message创建 Span。这些是:

  • MessagingSpanTextMapExtractor

  • MessagingSpanTextMapInjector

您可以通过提供自己的 implementation 来覆盖它们。

54.2 HTTP

对于 HTTP,有 2 个接口负责从Message创建 Span。这些是:

  • HttpSpanExtractor

  • HttpSpanInjector

您可以通过提供自己的 implementation 来覆盖它们。

54.3 示例

让我们假设您拥有的标准 Zipkin 兼容跟踪 HTTP 标头名称

  • 用于跟踪 ID - correlationId

  • for span id - mySpanId

这是一个SpanExtractor的示例

static class CustomHttpSpanExtractor implements HttpSpanExtractor {

	@Override public Span joinTrace(SpanTextMap carrier) {
		Map<String, String> map = TextMapUtil.asMap(carrier);
		long traceId = Span.hexToId(map.get("correlationid"));
		long spanId = Span.hexToId(map.get("myspanid"));
		// extract all necessary headers
		Span.SpanBuilder builder = Span.builder().traceId(traceId).spanId(spanId);
		// build rest of the Span
		return builder.build();
	}
}

static class CustomHttpSpanInjector implements HttpSpanInjector {

	@Override
	public void inject(Span span, SpanTextMap carrier) {
		carrier.put("correlationId", span.traceIdString());
		carrier.put("mySpanId", Span.idToHex(span.getSpanId()));
	}
}

你可以像这样注册:

@Bean
HttpSpanInjector customHttpSpanInjector() {
	return new CustomHttpSpanInjector();
}

@Bean
HttpSpanExtractor customHttpSpanExtractor() {
	return new CustomHttpSpanExtractor();
}

Spring Cloud Sleuth 出于安全原因,不会将与 trace/span 相关的 headers 添加到 Http 响应中。如果你需要 headers,那么将 headers 注入 Http Response 的自定义SpanInjector和使用它的 Servlet 过滤器可以通过以下方式添加:

static class CustomHttpServletResponseSpanInjector extends ZipkinHttpSpanInjector {

	@Override
	public void inject(Span span, SpanTextMap carrier) {
		super.inject(span, carrier);
		carrier.put(Span.TRACE_ID_NAME, span.traceIdString());
		carrier.put(Span.SPAN_ID_NAME, Span.idToHex(span.getSpanId()));
	}
}

static class HttpResponseInjectingTraceFilter extends GenericFilterBean {

	private final Tracer tracer;
	private final HttpSpanInjector spanInjector;

	public HttpResponseInjectingTraceFilter(Tracer tracer, HttpSpanInjector spanInjector) {
		this.tracer = tracer;
		this.spanInjector = spanInjector;
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
		HttpServletResponse response = (HttpServletResponse) servletResponse;
		Span currentSpan = this.tracer.getCurrentSpan();
		this.spanInjector.inject(currentSpan, new HttpServletResponseTextMap(response));
		filterChain.doFilter(request, response);
	}

	 class HttpServletResponseTextMap implements SpanTextMap {

		 private final HttpServletResponse delegate;

		 HttpServletResponseTextMap(HttpServletResponse delegate) {
			 this.delegate = delegate;
		 }

		 @Override
		 public Iterator<Map.Entry<String, String>> iterator() {
			 Map<String, String> map = new HashMap<>();
			 for (String header : this.delegate.getHeaderNames()) {
				map.put(header, this.delegate.getHeader(header));
			 }
			 return map.entrySet().iterator();
		 }

		 @Override
		 public void put(String key, String value) {
			this.delegate.addHeader(key, value);
		 }
	 }
}

你可以像这样注册它们:

@Bean HttpSpanInjector customHttpServletResponseSpanInjector() {
	return new CustomHttpServletResponseSpanInjector();
}

@Bean
HttpResponseInjectingTraceFilter responseInjectingTraceFilter(Tracer tracer) {
	return new HttpResponseInjectingTraceFilter(tracer, customHttpServletResponseSpanInjector());
}

54.4 TraceFilter

您还可以修改TraceFilter的行为 - 负责处理输入 HTTP 请求的 component,并根据 HTTP 响应添加标记。您可以通过注册自己的TraceFilter bean 实例来自定义标记或修改响应 headers。

在下面的 example 中,我们将注册TraceFilter bean,我们将添加包含当前 Span 的跟踪 ID 的ZIPKIN-TRACE-ID响应头。此外,我们将使用 key custom和 value tag添加 Span 标记。

@Bean
TraceFilter myTraceFilter(BeanFactory beanFactory, final Tracer tracer) {
	return new TraceFilter(beanFactory) {
		@Override protected void addResponseTags(HttpServletResponse response,
				Throwable e) {
			// execute the default behaviour
			super.addResponseTags(response, e);
			// for readability we're returning trace id in a hex form
			response.addHeader("ZIPKIN-TRACE-ID",
					Span.idToHex(tracer.getCurrentSpan().getTraceId()));
			// we can also add some custom tags
			tracer.addTag("custom", "tag");
		}
	};
}

要更改TraceFilter注册的 order,请设置spring.sleuth.web.filter-order property。

54.5 Zipkin 中的自定义 SA 标记

有时您想要创建一个手动 Span,它将包含对未检测的外部服务的调用。你可以做的是用peer.service标签创建一个 span,它将包含你要调用的服务的 value。下面你可以看到一个包含在这样一个 span 中的 Redis 调用的例子。

org.springframework.cloud.sleuth.Span newSpan = tracer.createSpan("redis");
try {
	newSpan.tag("redis.op", "get");
	newSpan.tag("lc", "redis");
	newSpan.logEvent(org.springframework.cloud.sleuth.Span.CLIENT_SEND);
	// call redis service e.g
	// return (SomeObj) redisTemplate.opsForHash().get("MYHASH", someObjKey);
} finally {
	newSpan.tag("peer.service", "redisService");
	newSpan.tag("peer.ipv4", "1.2.3.4");
	newSpan.tag("peer.port", "1234");
	newSpan.logEvent(org.springframework.cloud.sleuth.Span.CLIENT_RECV);
	tracer.close(newSpan);
}

切记不要同时添加peer.service标签和SA标签!你只需要添加peer.service

54.6 自定义服务 name

默认情况下 Sleuth 假设当您将 span 发送到 Zipkin 时,您希望 span 的服务 name 等于spring.application.name value。但情况并非总是如此。在某些情况下,您希望为来自 application 的所有 spans 显式提供不同的服务 name。要实现这一点,只需将以下 property 传递给 application 即可覆盖该 value(example 为foo service name):

spring.zipkin.service.name: foo

54.7 自定义报告的 spans

在报告 spans 到 e.g 之前。 Zipkin 你可能有兴趣以某种方式修改 span。您可以使用SpanAdjuster接口实现此目的。

用法示例:

在 Sleuth 中,我们使用固定的 name 生成 spans。有些用户希望根据标签的值修改 name。 SpanAdjuster接口的实现可用于更改 name。 例:

@Bean
SpanAdjuster customSpanAdjuster() {
    return span -> span.toBuilder().name(scrub(span.getName())).build();
}

这将导致在将报告的 span 发送到 Zipkin 之前更改它的 name。

在实际报告完成之前,应该 inject SpanAdjuster并允许 span 操作。

54.8 Host 定位器

在 order 中定义与特定 span 对应的 host,我们需要解析 host name 和 port。默认方法是从服务器 properties 中获取它。如果由于某种原因没有设置那些,那么我们试图从网络接口检索 host name。

如果启用了发现 client 并且更喜欢从服务注册表中的已注册实例中检索 host 地址,则必须设置 property(它适用于基于 HTTP 和流的 span 报告)。

spring.zipkin.locator.discovery.enabled: true