Customizations
Thanks to the SpanInjector
and SpanExtractor
you can customize the way spans are created and propagated.
There are currently two built-in ways to pass tracing information between processes:
via Spring Integration
via HTTP
Span ids are extracted from Zipkin-compatible (B3) headers (either Message
or HTTP headers), to start or join an existing trace. Trace information is injected into any outbound requests so the next hop can extract them.
The key change in comparison to the previous versions of Sleuth is that Sleuth is implementing the Open Tracing’s TextMap
notion. In Sleuth it’s called SpanTextMap
. Basically the idea is that any means of communication (e.g. message, http request, etc.) can be abstracted via a SpanTextMap
. This abstraction defines how one can insert data into the carrier and how to retrieve it from there. Thanks to this if you want to instrument a new HTTP library that uses a FooRequest
as a mean of sending HTTP requests then you have to create an implementation of a SpanTextMap
that delegates calls to FooRequest
in terms of retrieval and insertion of HTTP headers.
Spring Integration
For Spring Integration there are 2 interfaces responsible for creation of a Span from a Message
. These are:
MessagingSpanTextMapExtractor
MessagingSpanTextMapInjector
You can override them by providing your own implementation.
HTTP
For HTTP there are 2 interfaces responsible for creation of a Span from a Message
. These are:
HttpSpanExtractor
HttpSpanInjector
You can override them by providing your own implementation.
Example
Let’s assume that instead of the standard Zipkin compatible tracing HTTP header names you have
for trace id -
correlationId
for span id -
mySpanId
This is a an example of a 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()));
}
}
And you could register it like this:
@Bean
HttpSpanInjector customHttpSpanInjector() {
return new CustomHttpSpanInjector();
}
@Bean
HttpSpanExtractor customHttpSpanExtractor() {
return new CustomHttpSpanExtractor();
}
Spring Cloud Sleuth does not add trace/span related headers to the Http Response for security reasons. If you need the headers then a custom SpanInjector
that injects the headers into the Http Response and a Servlet filter which makes use of this can be added the following way:
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);
}
}
}
And you could register them like this:
@Bean HttpSpanInjector customHttpServletResponseSpanInjector() {
return new CustomHttpServletResponseSpanInjector();
}
@Bean
HttpResponseInjectingTraceFilter responseInjectingTraceFilter(Tracer tracer) {
return new HttpResponseInjectingTraceFilter(tracer, customHttpServletResponseSpanInjector());
}
Custom SA tag in Zipkin
Sometimes you want to create a manual Span that will wrap a call to an external service which is not instrumented. What you can do is to create a span with the peer.service
tag that will contain a value of the service that you want to call. Below you can see an example of a call to Redis that is wrapped in such a span.
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);
}
Important | Remember not to add both peer.service tag and the SA tag! You have to add only peer.service . |
---|---|
Custom service name
By default Sleuth assumes that when you send a span to Zipkin, you want the span’s service name to be equal to spring.application.name
value. That’s not always the case though. There are situations in which you want to explicitly provide a different service name for all spans coming from your application. To achieve that it’s enough to just pass the following property to your application to override that value (example for foo
service name):
spring.zipkin.service.name: foo
Customization of reported spans
Before reporting spans to e.g. Zipkin you can be interested in modifying that span in some way. You can achieve that by using the SpanAdjuster
interface.
Example of usage:
In Sleuth we’re generating spans with a fixed name. Some users want to modify the name depending on values of tags. Implementation of the SpanAdjuster
interface can be used to alter that name. Example:
@Bean
SpanAdjuster customSpanAdjuster() {
return span -> span.toBuilder().name(scrub(span.getName())).build();
}
This will lead in changing the name of the reported span just before it gets sent to Zipkin.
Important | Your SpanReporter should inject the SpanAdjuster and allow span manipulation before the actual reporting is done. |
---|---|
Host locator
In order to define the host that is corresponding to a particular span we need to resolve the host name and port. The default approach is to take it from server properties. If those for some reason are not set then we’re trying to retrieve the host name from the network interfaces.
If you have the discovery client enabled and prefer to retrieve the host address from the registered instance in a service registry then you have to set the property (it’s applicable for both HTTP and Stream based span reporting).
spring.zipkin.locator.discovery.enabled: true