Skip to content

Commit

Permalink
Picks tracing data from B3 if present; fixes gh-1077
Browse files Browse the repository at this point in the history
  • Loading branch information
marcingrzejszczak committed Sep 7, 2018
1 parent d1dba30 commit b7302a6
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 21 deletions.
6 changes: 6 additions & 0 deletions docs/src/main/asciidoc/spring-cloud-sleuth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ 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 default way of coding tracing context is done via the `b3` header that contains the
`traceId-spanId-sampled` notation (e.g. `0000000000000005-0000000000000004-1`).
For backward compatibility, if the `b3` header is not present, we also check if
`X-B3` entries are present, and retrieve tracing context from there e.g.
(`X-B3-TraceId: 0000000000000005`, `X-B3-SpanId: 0000000000000004`, `X-B3-Sampled: 1`).

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class Span implements SpanContext {

public static final String B3_NAME = "b3";
public static final String SAMPLED_NAME = "X-B3-Sampled";
public static final String PROCESS_ID_NAME = "X-Process-Id";
public static final String PARENT_ID_NAME = "X-B3-ParentSpanId";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanTextMap;
import org.springframework.cloud.sleuth.util.TextMapUtil;
import org.springframework.util.StringUtils;

/**
* Default implementation for messaging
Expand All @@ -20,8 +21,8 @@ public class HeaderBasedMessagingExtractor implements MessagingSpanTextMapExtrac
@Override
public Span joinTrace(SpanTextMap textMap) {
Map<String, String> carrier = TextMapUtil.asMap(textMap);
boolean spanIdMissing = !hasHeader(carrier, TraceMessageHeaders.SPAN_ID_NAME);
boolean traceIdMissing = !hasHeader(carrier, TraceMessageHeaders.TRACE_ID_NAME);
boolean spanIdMissing = !hasSpanId(carrier);
boolean traceIdMissing = !hasTraceId(carrier);
if (Span.SPAN_SAMPLED.equals(carrier.get(TraceMessageHeaders.SPAN_FLAGS_NAME))) {
String traceId = generateTraceIdIfMissing(carrier, traceIdMissing);
if (spanIdMissing) {
Expand All @@ -35,24 +36,35 @@ public Span joinTrace(SpanTextMap textMap) {
return extractSpanFromHeaders(carrier, Span.builder(), idMissing);
}

private boolean hasTraceId(Map<String, String> carrier) {
return hasHeader(carrier, TraceMessageHeaders.B3_NAME) ||
hasHeader(carrier, TraceMessageHeaders.TRACE_ID_NAME);
}

private boolean hasSpanId(Map<String, String> carrier) {
return hasHeader(carrier, TraceMessageHeaders.B3_NAME) ||
hasHeader(carrier, TraceMessageHeaders.SPAN_ID_NAME);
}

private String generateTraceIdIfMissing(Map<String, String> carrier,
boolean traceIdMissing) {
if (traceIdMissing) {
carrier.put(TraceMessageHeaders.TRACE_ID_NAME, Span.idToHex(this.random.nextLong()));
long id = this.random.nextLong();
carrier.put(TraceMessageHeaders.TRACE_ID_NAME, Span.idToHex(id));
}
return carrier.get(TraceMessageHeaders.TRACE_ID_NAME);
return traceId(carrier);
}

private Span extractSpanFromHeaders(Map<String, String> carrier,
Span.SpanBuilder spanBuilder, boolean idMissing) {
String traceId = carrier.get(TraceMessageHeaders.TRACE_ID_NAME);
String traceId = traceId(carrier);
spanBuilder = spanBuilder
.traceIdHigh(traceId.length() == 32 ? Span.hexToId(traceId, 0) : 0)
.traceId(Span.hexToId(traceId))
.spanId(Span.hexToId(carrier.get(TraceMessageHeaders.SPAN_ID_NAME)));
.spanId(Span.hexToId(spanId(carrier)));
String flags = carrier.get(TraceMessageHeaders.SPAN_FLAGS_NAME);
boolean debug = Span.SPAN_SAMPLED.equals(flags);
boolean spanSampled = Span.SPAN_SAMPLED.equals(carrier.get(TraceMessageHeaders.SAMPLED_NAME));
boolean spanSampled = Span.SPAN_SAMPLED.equals(sampled(carrier));
if (debug) {
spanBuilder.exportable(true);
} else {
Expand All @@ -77,6 +89,39 @@ private Span extractSpanFromHeaders(Map<String, String> carrier,
return spanBuilder.build();
}

private String traceId(Map<String, String> carrier) {
String b3 = carrier.get(TraceMessageHeaders.B3_NAME);
if (StringUtils.hasText(b3)) {
String[] split = b3.split("-");
if (split.length == 3) {
return split[0];
}
}
return carrier.get(TraceMessageHeaders.TRACE_ID_NAME);
}

private String spanId(Map<String, String> carrier) {
String b3 = carrier.get(TraceMessageHeaders.B3_NAME);
if (StringUtils.hasText(b3)) {
String[] split = b3.split("-");
if (split.length == 3) {
return split[1];
}
}
return carrier.get(TraceMessageHeaders.SPAN_ID_NAME);
}

private String sampled(Map<String, String> carrier) {
String b3 = carrier.get(TraceMessageHeaders.B3_NAME);
if (StringUtils.hasText(b3)) {
String[] split = b3.split("-");
if (split.length == 3) {
return split[2];
}
}
return carrier.get(TraceMessageHeaders.SAMPLED_NAME);
}

boolean hasHeader(Map<String, String> message, String name) {
return message.containsKey(name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ private boolean isSampled(Map<String, String> initialMessage, String sampledHead
private void addHeaders(Map<String, String> map, Span span, SpanTextMap textMap) {
addHeader(map, textMap, TraceMessageHeaders.TRACE_ID_NAME, span.traceIdString());
addHeader(map, textMap, TraceMessageHeaders.SPAN_ID_NAME, Span.idToHex(span.getSpanId()));
addHeader(map, textMap, TraceMessageHeaders.B3_NAME, span.traceIdString() + "-" +
Span.idToHex(span.getSpanId()) + "-" + (span.isExportable() ? Span.SPAN_SAMPLED : Span.SPAN_NOT_SAMPLED));
if (span.isExportable()) {
addAnnotations(this.traceKeys, textMap, span);
Long parentId = getFirst(span.getParents());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/
public class TraceMessageHeaders {

public static final String B3_NAME = "b3";
public static final String SPAN_ID_NAME = "spanId";
public static final String SAMPLED_NAME = "spanSampled";
public static final String PROCESS_ID_NAME = "spanProcessId";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,19 @@ private boolean onlySpanIdIsPresent(Map<String, String> carrier) {
}

private boolean traceIdIsMissing(Map<String, String> carrier) {
return carrier.get(Span.TRACE_ID_NAME) == null;
return traceId(carrier) == null;
}

private boolean spanIdIsPresent(Map<String, String> carrier) {
return carrier.get(Span.SPAN_ID_NAME) != null;
return spanId(carrier) != null;
}

private String generateId() {
return Span.idToHex(this.random.nextLong());
}

private long spanId(String spanId, String traceId) {
private long spanIdOrDefault(Map<String, String> carrier, String traceId) {
String spanId = spanId(carrier);
if (spanId == null) {
if (log.isDebugEnabled()) {
log.debug("Request is missing a span id but it has a trace id. We'll assume that this is "
Expand All @@ -80,15 +81,53 @@ private long spanId(String spanId, String traceId) {
}
}

private Span buildParentSpan(Map<String, String> carrier, boolean idToBeGenerated) {
String traceId = carrier.get(Span.TRACE_ID_NAME);
private String traceId(Map<String, String> carrier) {
String b3 = carrier.get(Span.B3_NAME);
if (StringUtils.hasText(b3)) {
String[] split = b3.split("-");
if (split.length == 3) {
return split[0];
}
}
return carrier.get(Span.TRACE_ID_NAME);
}

private String spanId(Map<String, String> carrier) {
String b3 = carrier.get(Span.B3_NAME);
if (StringUtils.hasText(b3)) {
String[] split = b3.split("-");
if (split.length == 3) {
return split[1];
}
}
return carrier.get(Span.SPAN_ID_NAME);
}

private String sampled(Map<String, String> carrier) {
String b3 = carrier.get(Span.B3_NAME);
if (StringUtils.hasText(b3)) {
String[] split = b3.split("-");
if (split.length == 3) {

This comment has been minimized.

Copy link
@shakuzen

shakuzen Sep 7, 2018

Contributor

I think sampled could also be the only field, for instance when just sending an unsampled decision 0. It's one of the examples here: openzipkin/b3-propagation#21 (comment)
Also, it could be d in the third position when the debug flag is set, which this method will parse but the caller doesn't seem to handle it currently.

This comment has been minimized.

Copy link
@shakuzen

shakuzen Sep 7, 2018

Contributor

Another thing is the case when a parentId is sent doesn't seem to be handled (there will be 4 --separated fields, not 3). It can be omitted for the messaging extractor/injector, I guess, but the HTTP implementation seems like it should handle it.

This comment has been minimized.

Copy link
@marcingrzejszczak

marcingrzejszczak Sep 7, 2018

Author Contributor

Thanks for the hint. I thought that traceid-spanid-sampled is the only option. Let me reopen the issue

This comment has been minimized.

Copy link
@codefromthecrypt

codefromthecrypt Sep 8, 2018

Contributor

yep agree with Tomás. a single character of 1, 0 or d should work.

return split[2];
}
}
return carrier.get(Span.SAMPLED_NAME);
}

private String traceIdOrDefaut(Map<String, String> carrier) {
String traceId = traceId(carrier);
if (traceId == null) {
traceId = generateId();
}
return traceId;
}

private Span buildParentSpan(Map<String, String> carrier, boolean idToBeGenerated) {
String traceId = traceIdOrDefaut(carrier);
Span.SpanBuilder span = Span.builder()
.traceIdHigh(traceId.length() == 32 ? Span.hexToId(traceId, 0) : 0)
.traceId(Span.hexToId(traceId))
.spanId(spanId(carrier.get(Span.SPAN_ID_NAME), traceId));
.spanId(spanIdOrDefault(carrier, traceId));
String parentName = carrier.get(Span.SPAN_NAME_NAME);
if (StringUtils.hasText(parentName)) {
span.name(parentName);
Expand All @@ -108,7 +147,7 @@ private Span buildParentSpan(Map<String, String> carrier, boolean idToBeGenerate

boolean skip = this.skipPattern
.matcher(carrier.get(ZipkinHttpSpanMapper.URI_HEADER)).matches()
|| Span.SPAN_NOT_SAMPLED.equals(carrier.get(Span.SAMPLED_NAME));
|| Span.SPAN_NOT_SAMPLED.equals(sampled(carrier));
// trace, span id were retrieved from the headers and span is sampled
span.shared(!(skip || idToBeGenerated));
boolean debug = Span.SPAN_SAMPLED.equals(carrier.get(Span.SPAN_FLAGS));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class ZipkinHttpSpanInjector implements HttpSpanInjector {
@Override
public void inject(Span span, SpanTextMap map) {
Map<String, String> carrier = SPAN_CARRIER_MAPPER.convert(map);
setHeader(map, carrier, Span.B3_NAME, b3(span));
setHeader(map, carrier, Span.TRACE_ID_NAME, span.traceIdString());
setIdHeader(map, carrier, Span.SPAN_ID_NAME, span.getSpanId());
setHeader(map, carrier, Span.SAMPLED_NAME, span.isExportable() ? Span.SPAN_SAMPLED : Span.SPAN_NOT_SAMPLED);
Expand All @@ -30,6 +31,11 @@ public void inject(Span span, SpanTextMap map) {
}
}

private String b3(Span span) {
return span.traceIdString() + "-" + Span.idToHex(span.getSpanId()) + "-" +
(span.isExportable() ? Span.SPAN_SAMPLED : Span.SPAN_NOT_SAMPLED);
}

private String prefixedKey(String key) {
if (key.startsWith(Span.SPAN_BAGGAGE_HEADER_PREFIX
+ ZipkinHttpSpanMapper.HEADER_DELIMITER)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public int compare(String o1, String o2) {

static {
TreeSet<String> fields = new TreeSet<>(IGNORE_CASE_COMPARATOR);
Collections.addAll(fields, Span.SPAN_FLAGS, Span.TRACE_ID_NAME, Span.SPAN_ID_NAME,
Collections.addAll(fields, Span.B3_NAME,
Span.SPAN_FLAGS, Span.TRACE_ID_NAME, Span.SPAN_ID_NAME,
Span.PROCESS_ID_NAME, Span.SPAN_NAME_NAME, Span.PARENT_ID_NAME,
Span.SAMPLED_NAME, URI_HEADER);
SPAN_FIELDS = Collections.unmodifiableSet(fields);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ public SpanAssert hasTraceIdEqualTo(Long traceId) {
return this;
}

public SpanAssert hasSpanIdEqualTo(Long spanId) {
isNotNull();
if (!Objects.equals(this.actual.getSpanId(), spanId)) {
String message = String.format("Expected span's spanId to be <%s> but was <%s>", spanId, this.actual.getSpanId());
log.error(message);
failWithMessage(message);
}
return this;
}

public SpanAssert hasNameEqualTo(String name) {
isNotNull();
if (!Objects.equals(this.actual.getName(), name)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package org.springframework.cloud.sleuth.instrument.messaging;

import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
Expand All @@ -26,11 +24,49 @@
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanTextMap;

import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then;

/**
* @author Marcin Grzejszczak
*/
public class HeaderBasedMessagingExtractorTests {

@Test
public void b3HeadersTakePrecedenceOverAnyOtherHeaders() {
HeaderBasedMessagingExtractor extractor = new HeaderBasedMessagingExtractor();
SpanTextMap spanTextMap = spanTextMap();
spanTextMap.put(TraceMessageHeaders.B3_NAME, "0000000000000005-0000000000000004-1");
spanTextMap.put(TraceMessageHeaders.SPAN_ID_NAME, Span.idToHex(20L));
spanTextMap.put(TraceMessageHeaders.TRACE_ID_NAME, Span.idToHex(30L));
spanTextMap.put(TraceMessageHeaders.SAMPLED_NAME, "0");

Span span = extractor.joinTrace(spanTextMap);

then(span)
.isNotNull()
.hasTraceIdEqualTo(5L)
.hasSpanIdEqualTo(4L)
.isExportable();
}

@Test
public void legacyHeadersTakePrecedenceOverB3WhenB3IsInvalid() {
HeaderBasedMessagingExtractor extractor = new HeaderBasedMessagingExtractor();
SpanTextMap spanTextMap = spanTextMap();
spanTextMap.put(TraceMessageHeaders.B3_NAME, "invalid");
spanTextMap.put(TraceMessageHeaders.SPAN_ID_NAME, Span.idToHex(20L));
spanTextMap.put(TraceMessageHeaders.TRACE_ID_NAME, Span.idToHex(30L));
spanTextMap.put(TraceMessageHeaders.SAMPLED_NAME, "0");

Span span = extractor.joinTrace(spanTextMap);

then(span)
.isNotNull()
.hasTraceIdEqualTo(30L)
.hasSpanIdEqualTo(20L)
.isNotExportable();
}

@Test
public void overridesTheSampleFlagWithSpanFlagForSampledScenario() {
HeaderBasedMessagingExtractor extractor = new HeaderBasedMessagingExtractor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public void should_not_override_already_existing_headers() throws Exception {
injector.inject(span, map);

then(map)
.contains(new AbstractMap.SimpleEntry<String, String>(TraceMessageHeaders.B3_NAME, "0000000000000002-0000000000000001-1"))
.contains(new AbstractMap.SimpleEntry<String, String>(TraceMessageHeaders.SPAN_ID_NAME, Span.idToHex(10L)))
.contains(new AbstractMap.SimpleEntry<String, String>(TraceMessageHeaders.TRACE_ID_NAME, Span.idToHex(20L)))
.contains(new AbstractMap.SimpleEntry<String, String>(TraceMessageHeaders.PARENT_ID_NAME, Span.idToHex(30L)))
Expand Down
Loading

0 comments on commit b7302a6

Please sign in to comment.