Picks tracing data from B3 if present; fixes gh-1077

This commit is contained in:
Marcin Grzejszczak
2018-09-07 16:19:07 +02:00
parent d1dba30086
commit b7302a61f6
13 changed files with 222 additions and 21 deletions

View File

@@ -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

View File

@@ -75,6 +75,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
@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";

View File

@@ -6,6 +6,7 @@ import java.util.Random;
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
@@ -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) {
@@ -35,24 +36,35 @@ public class HeaderBasedMessagingExtractor implements MessagingSpanTextMapExtrac
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 {
@@ -77,6 +89,39 @@ public class HeaderBasedMessagingExtractor implements MessagingSpanTextMapExtrac
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);
}

View File

@@ -43,6 +43,8 @@ public class HeaderBasedMessagingInjector implements MessagingSpanTextMapInjecto
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());

View File

@@ -29,6 +29,7 @@ import java.util.List;
*/
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";

View File

@@ -57,18 +57,19 @@ public class ZipkinHttpSpanExtractor implements HttpSpanExtractor {
}
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 "
@@ -80,15 +81,53 @@ public class ZipkinHttpSpanExtractor implements HttpSpanExtractor {
}
}
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) {
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);
@@ -108,7 +147,7 @@ public class ZipkinHttpSpanExtractor implements HttpSpanExtractor {
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));

View File

@@ -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);
@@ -30,6 +31,11 @@ public class ZipkinHttpSpanInjector implements HttpSpanInjector {
}
}
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)) {

View File

@@ -52,7 +52,8 @@ class ZipkinHttpSpanMapper {
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);

View File

@@ -45,6 +45,16 @@ public class SpanAssert extends AbstractAssert<SpanAssert, Span> {
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)) {

View File

@@ -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;
@@ -26,11 +24,49 @@ import org.junit.Test;
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();

View File

@@ -40,6 +40,7 @@ public class HeaderBasedMessagingInjectorTests {
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)))

View File

@@ -16,13 +16,11 @@
package org.springframework.cloud.sleuth.instrument.web;
import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Random;
import java.util.Vector;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Test;
@@ -32,6 +30,8 @@ import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.cloud.sleuth.Span;
import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then;
@RunWith(MockitoJUnitRunner.class)
public class HttpServletRequestExtractorTests {
@@ -80,11 +80,62 @@ public class HttpServletRequestExtractorTests {
then(this.extractor.joinTrace(new HttpServletRequestTextMap(this.request))).isNull();
}
@Test
public void should_pick_values_from_b3_if_present() {
BDDMockito.given(this.request.getHeaderNames())
.willReturn(new Vector<>(Arrays.asList(Span.B3_NAME, Span.TRACE_ID_NAME,
Span.SPAN_ID_NAME, Span.PARENT_ID_NAME, Span.SAMPLED_NAME,
Span.SPAN_FLAGS)).elements());
BDDMockito.given(this.request.getHeader(Span.TRACE_ID_NAME))
.willReturn(Span.idToHex(10L));
BDDMockito.given(this.request.getHeader(Span.SPAN_ID_NAME))
.willReturn(Span.idToHex(20L));
BDDMockito.given(this.request.getHeader(Span.PARENT_ID_NAME))
.willReturn(Span.idToHex(30L));
BDDMockito.given(this.request.getHeader(Span.SAMPLED_NAME))
.willReturn(Span.SPAN_NOT_SAMPLED);
BDDMockito.given(this.request.getHeader(Span.B3_NAME))
.willReturn("0000000000000005-0000000000000004-1");
Span span = this.extractor.joinTrace(new HttpServletRequestTextMap(this.request));
then(span)
.isNotNull()
.hasTraceIdEqualTo(5L)
.hasSpanIdEqualTo(4L)
.isExportable();
}
@Test
public void should_pick_values_from_old_headers_when_b3_is_invalid() {
BDDMockito.given(this.request.getHeaderNames())
.willReturn(new Vector<>(Arrays.asList(Span.B3_NAME, Span.TRACE_ID_NAME,
Span.SPAN_ID_NAME, Span.PARENT_ID_NAME, Span.SAMPLED_NAME,
Span.SPAN_FLAGS)).elements());
BDDMockito.given(this.request.getHeader(Span.TRACE_ID_NAME))
.willReturn(Span.idToHex(10L));
BDDMockito.given(this.request.getHeader(Span.SPAN_ID_NAME))
.willReturn(Span.idToHex(20L));
BDDMockito.given(this.request.getHeader(Span.PARENT_ID_NAME))
.willReturn(Span.idToHex(30L));
BDDMockito.given(this.request.getHeader(Span.SAMPLED_NAME))
.willReturn(Span.SPAN_NOT_SAMPLED);
BDDMockito.given(this.request.getHeader(Span.B3_NAME))
.willReturn("invalid");
Span span = this.extractor.joinTrace(new HttpServletRequestTextMap(this.request));
then(span)
.isNotNull()
.hasTraceIdEqualTo(10L)
.hasSpanIdEqualTo(20L)
.isNotExportable();
}
@Test
public void should_accept_128bit_trace_id() {
String hex128Bits = spanInHeaders();
Span span = this.extractor.joinTrace(new HttpServletRequestTextMap(this.request));
then(span.traceIdString()).isEqualTo(hex128Bits);

View File

@@ -38,6 +38,8 @@ public class ZipkinHttpSpanInjectorTests {
injector.inject(span, map);
then(map)
.contains(new AbstractMap.SimpleEntry<String, String>(Span.B3_NAME,
"0000000000000002-0000000000000001-1"))
.contains(new AbstractMap.SimpleEntry<String, String>(Span.SPAN_ID_NAME, Span.idToHex(10L)))
.contains(new AbstractMap.SimpleEntry<String, String>(Span.TRACE_ID_NAME, Span.idToHex(20L)))
.contains(new AbstractMap.SimpleEntry<String, String>(Span.PARENT_ID_NAME, Span.idToHex(30L)))