Added baggage support (#440)

without this change there is no support for context propagation
with this change whenever you pass the `baggage-...` for http or `baggage_` for messaging headers then such a value will be propagated through your system

fixes #237
This commit is contained in:
Marcin Grzejszczak
2016-11-10 15:36:47 +01:00
committed by GitHub
parent 9ab37c34fa
commit 96df523557
19 changed files with 456 additions and 90 deletions

View File

@@ -28,13 +28,13 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Class for gathering and reporting statistics about a block of execution.
* <p>
@@ -73,7 +73,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
*/
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class Span {
public class Span implements SpanContext {
public static final String SAMPLED_NAME = "X-B3-Sampled";
public static final String PROCESS_ID_NAME = "X-Process-Id";
@@ -82,6 +82,7 @@ public class Span {
public static final String SPAN_NAME_NAME = "X-Span-Name";
public static final String SPAN_ID_NAME = "X-B3-SpanId";
public static final String SPAN_EXPORT_NAME = "X-Span-Export";
public static final String SPAN_BAGGAGE_HEADER_PREFIX = "baggage";
public static final Set<String> SPAN_HEADERS = new HashSet<>(
Arrays.asList(SAMPLED_NAME, PROCESS_ID_NAME, PARENT_ID_NAME, TRACE_ID_NAME,
SPAN_ID_NAME, SPAN_NAME_NAME, SPAN_EXPORT_NAME));
@@ -146,6 +147,8 @@ public class Span {
private final String processId;
private final Collection<Log> logs;
private final Span savedSpan;
@JsonIgnore
private final Map<String,String> baggage;
// Null means we don't know the start tick, so fallback to time
@JsonIgnore
@@ -176,6 +179,7 @@ public class Span {
this.logs = current.logs;
this.startNanos = current.startNanos;
this.durationMicros = current.durationMicros;
this.baggage = current.baggage;
this.savedSpan = savedSpan;
}
@@ -209,6 +213,7 @@ public class Span {
this.savedSpan = savedSpan;
this.tags = new ConcurrentHashMap<>();
this.logs = new ConcurrentLinkedQueue<>();
this.baggage = new ConcurrentHashMap<>();
}
public static SpanBuilder builder() {
@@ -304,6 +309,40 @@ public class Span {
this.logs.add(new Log(System.currentTimeMillis(), event));
}
/**
* Sets a baggage item in the Span (and its SpanContext) as a key/value pair.
*
* Baggage enables powerful distributed context propagation functionality where arbitrary application data can be
* carried along the full path of request execution throughout the system.
*
* Note 1: Baggage is only propagated to the future (recursive) children of this SpanContext.
*
* Note 2: Baggage is sent in-band with every subsequent local and remote calls, so this feature must be used with
* care.
*
* @return this Span instance, for chaining
*/
public Span setBaggageItem(String key, String value) {
this.baggage.put(key, value);
return this;
}
/**
* @return the value of the baggage item identified by the given key, or null if no such item could be found
*/
public String getBaggageItem(String key) {
return this.baggage.get(key);
}
@Override
public final Iterable<Map.Entry<String,String>> baggageItems() {
return this.baggage.entrySet();
}
public final Map<String,String> getBaggage() {
return Collections.unmodifiableMap(this.baggage);
}
/**
* Get tag data associated with this span (read only)
* <p/>
@@ -493,6 +532,7 @@ public class Span {
private Span savedSpan;
private List<Log> logs = new ArrayList<>();
private Map<String, String> tags = new LinkedHashMap<>();
private Map<String, String> baggage = new LinkedHashMap<>();
SpanBuilder() {
}
@@ -554,6 +594,16 @@ public class Span {
return this;
}
public Span.SpanBuilder baggage(String baggageKey, String baggageValue) {
this.baggage.put(baggageKey, baggageValue);
return this;
}
public Span.SpanBuilder baggage(Map<String, String> baggage) {
this.baggage.putAll(baggage);
return this;
}
public Span.SpanBuilder spanId(long spanId) {
this.spanId = spanId;
return this;
@@ -585,6 +635,7 @@ public class Span {
this.processId, this.savedSpan);
span.logs.addAll(this.logs);
span.tags.putAll(this.tags);
span.baggage.putAll(this.baggage);
return span;
}

View File

@@ -0,0 +1,31 @@
package org.springframework.cloud.sleuth;
import java.util.Map;
/**
* Adopted from: https://github.com/opentracing/opentracing-java/blob/0.16.0/opentracing-api/src/main/java/io/opentracing/SpanContext.java
*
* SpanContext represents Span state that must propagate to descendant Spans and across process boundaries.
*
* SpanContext is logically divided into two pieces: (1) the user-level "Baggage" that propagates across Span
* boundaries and (2) any Tracer-implementation-specific fields that are needed to identify or otherwise contextualize
* the associated Span instance (e.g., a <trace_id, span_id, sampled> tuple).
*
* The {@link SpanContext#baggageItems()} returns the map of user-level "Baggage".
*
* @see Span#setBaggageItem(String, String)
* @see Span#getBaggageItem(String)
*
* @author Marcin Grzejszczak
* @since 1.2.0
*/
public interface SpanContext {
/**
* @return all zero or more baggage items propagating along with the associated Span
*
* @see Span#setBaggageItem(String, String)
* @see Span#getBaggageItem(String)
*/
Iterable<Map.Entry<String, String>> baggageItems();
}

View File

@@ -4,7 +4,7 @@ import java.util.Iterator;
import java.util.Map;
/**
* Adopted from: https://github.com/opentracing/opentracing-java/blob/master/opentracing-api/src/main/java/io/opentracing/propagation/TextMap.java
* Adopted from: https://github.com/opentracing/opentracing-java/blob/0.16.0/opentracing-api/src/main/java/io/opentracing/propagation/TextMap.java
*
* TextMap is a built-in carrier for {@link SpanInjector} and {@link SpanExtractor}. TextMap implementations allows Tracers to
* read and write key:value String pairs from arbitrary underlying sources of data.

View File

@@ -43,6 +43,11 @@ public class HeaderBasedMessagingExtractor implements MessagingSpanTextMapExtrac
}
setParentIdIfApplicable(carrier, spanBuilder, TraceMessageHeaders.PARENT_ID_NAME);
spanBuilder.remote(true);
for (Map.Entry<String, String> entry : carrier.entrySet()) {
if (entry.getKey().startsWith(Span.SPAN_BAGGAGE_HEADER_PREFIX + TraceMessageHeaders.HEADER_DELIMITER)) {
spanBuilder.baggage(unprefixedKey(entry.getKey()), entry.getValue());
}
}
return spanBuilder.build();
}
@@ -58,4 +63,8 @@ public class HeaderBasedMessagingExtractor implements MessagingSpanTextMapExtrac
}
}
private String unprefixedKey(String key) {
return key.substring(key.indexOf(TraceMessageHeaders.HEADER_DELIMITER) + 1);
}
}

View File

@@ -56,6 +56,9 @@ public class HeaderBasedMessagingInjector implements MessagingSpanTextMapInjecto
else {
addHeader(textMap, TraceMessageHeaders.SAMPLED_NAME, Span.SPAN_NOT_SAMPLED);
}
for (Map.Entry<String, String> entry : span.baggageItems()) {
textMap.put(prefixedKey(entry.getKey()), entry.getValue());
}
}
private void addAnnotations(TraceKeys traceKeys, SpanTextMap spanTextMap, Span span) {
@@ -99,4 +102,11 @@ public class HeaderBasedMessagingInjector implements MessagingSpanTextMapInjecto
return parents.isEmpty() ? null : parents.get(0);
}
private String prefixedKey(String key) {
if (key.startsWith(Span.SPAN_BAGGAGE_HEADER_PREFIX + TraceMessageHeaders.HEADER_DELIMITER )) {
return key;
}
return Span.SPAN_BAGGAGE_HEADER_PREFIX + TraceMessageHeaders.HEADER_DELIMITER + key;
}
}

View File

@@ -34,6 +34,7 @@ public class TraceMessageHeaders {
public static final String SPAN_NAME_NAME = "spanName";
static final String MESSAGE_SENT_FROM_CLIENT = "messageSent";
static final String HEADER_DELIMITER = "_";
private TraceMessageHeaders() {}
}

View File

@@ -20,6 +20,7 @@ public class ZipkinHttpSpanExtractor implements HttpSpanExtractor {
private static final org.apache.commons.logging.Log log = LogFactory.getLog(
MethodHandles.lookup().lookupClass());
private static final String HEADER_DELIMITER = "-";
static final String URI_HEADER = "X-Span-Uri";
private static final String HTTP_COMPONENT = "http";
@@ -83,7 +84,16 @@ public class ZipkinHttpSpanExtractor implements HttpSpanExtractor {
if (skip) {
span.exportable(false);
}
for (Map.Entry<String, String> entry : carrier.entrySet()) {
if (entry.getKey().startsWith(Span.SPAN_BAGGAGE_HEADER_PREFIX + HEADER_DELIMITER)) {
span.baggage(unprefixedKey(entry.getKey()), entry.getValue());
}
}
return span.build();
}
private String unprefixedKey(String key) {
return key.substring(key.indexOf(HEADER_DELIMITER) + 1);
}
}

View File

@@ -1,5 +1,7 @@
package org.springframework.cloud.sleuth.instrument.web;
import java.util.Map;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanTextMap;
import org.springframework.util.StringUtils;
@@ -12,6 +14,8 @@ import org.springframework.util.StringUtils;
*/
public class ZipkinHttpSpanInjector implements HttpSpanInjector {
private static final String HEADER_DELIMITER = "-";
@Override
public void inject(Span span, SpanTextMap carrier) {
setIdHeader(carrier, Span.TRACE_ID_NAME, span.getTraceId());
@@ -20,22 +24,32 @@ public class ZipkinHttpSpanInjector implements HttpSpanInjector {
setHeader(carrier, Span.SPAN_NAME_NAME, span.getName());
setIdHeader(carrier, Span.PARENT_ID_NAME, getParentId(span));
setHeader(carrier, Span.PROCESS_ID_NAME, span.getProcessId());
for (Map.Entry<String, String> entry : span.baggageItems()) {
carrier.put(prefixedKey(entry.getKey()), entry.getValue());
}
}
private String prefixedKey(String key) {
if (key.startsWith(Span.SPAN_BAGGAGE_HEADER_PREFIX + HEADER_DELIMITER)) {
return key;
}
return Span.SPAN_BAGGAGE_HEADER_PREFIX + HEADER_DELIMITER + key;
}
private Long getParentId(Span span) {
return !span.getParents().isEmpty() ? span.getParents().get(0) : null;
}
private void setHeader(SpanTextMap carrier, String name, String value) {
if (StringUtils.hasText(value)) {
carrier.put(name, value);
}
}
private void setIdHeader(SpanTextMap carrier, String name, Long value) {
if (value != null) {
setHeader(carrier, name, Span.idToHex(value));
}
}
private void setHeader(SpanTextMap carrier, String name, String value) {
if (StringUtils.hasText(value)) {
carrier.put(name, value);
}
}
}

View File

@@ -138,7 +138,7 @@ public class DefaultTracer implements Tracer {
return savedSpan;
}
protected Span createChild(Span parent, String name) {
Span createChild(Span parent, String name) {
long id = createId();
if (parent == null) {
Span span = Span.builder().name(name)
@@ -154,7 +154,9 @@ public class DefaultTracer implements Tracer {
Span span = Span.builder().name(name)
.traceId(parent.getTraceId()).parent(parent.getSpanId()).spanId(id)
.processId(parent.getProcessId()).savedSpan(parent)
.exportable(parent.isExportable()).build();
.exportable(parent.isExportable())
.baggage(parent.getBaggage())
.build();
this.spanLogger.logStartedSpan(parent, span);
return span;
}

View File

@@ -51,4 +51,10 @@ public class ArrayListSpanAccumulator implements SpanReporter {
this.spans.add(span);
}
}
public void clear() {
synchronized (this.spans) {
this.spans.clear();
}
}
}

View File

@@ -21,14 +21,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.assertj.core.api.AbstractAssert;
import org.springframework.cloud.sleuth.Span;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
@@ -89,6 +88,26 @@ public class ListOfSpansAssert extends AbstractAssert<ListOfSpansAssert, ListOfS
return this;
}
public ListOfSpansAssert everySpanHasABaggage(String baggageKey, String baggageValue) {
isNotNull();
printSpans();
if (!everySpanHasBaggage(baggageKey, baggageValue)) {
failWithMessage("Expected spans \n <%s> \nto ALL contain baggage with key "
+ "equal to <%s>, and value equal to <%s>", spansToString(), baggageKey, baggageValue);
}
return this;
}
public ListOfSpansAssert anySpanHasABaggage(String baggageKey, String baggageValue) {
isNotNull();
printSpans();
if (!hasBaggage(baggageKey, baggageValue)) {
failWithMessage("Expected spans \n <%s> \nto contain at least one span with baggage key "
+ "equal to <%s>, and value equal to <%s>", spansToString(), baggageKey, baggageValue);
}
return this;
}
private boolean spanWithKeyTagExists(String tagKey) {
for (Span span : this.actual.spans) {
if (span.tags().containsKey(tagKey)) {
@@ -98,6 +117,37 @@ public class ListOfSpansAssert extends AbstractAssert<ListOfSpansAssert, ListOfS
return false;
}
private boolean everySpanHasBaggage(String baggageKey, String baggageValue) {
boolean exists = false;
for (Span span : this.actual.spans) {
for (Map.Entry<String, String> baggage : span.baggageItems()) {
if (baggage.getKey().equals(baggageKey)) {
if (baggage.getValue().equals(baggageValue)) {
exists = true;
break;
}
}
}
if (!exists) {
return false;
}
}
return exists;
}
private boolean hasBaggage(String baggageKey, String baggageValue) {
for (Span span : this.actual.spans) {
for (Map.Entry<String, String> baggage : span.baggageItems()) {
if (baggage.getKey().equals(baggageKey)) {
if (baggage.getValue().equals(baggageValue)) {
return true;
}
}
}
}
return false;
}
public ListOfSpansAssert hasASpanWithTagEqualTo(String tagKey, String tagValue) {
isNotNull();
printSpans();
@@ -113,7 +163,8 @@ public class ListOfSpansAssert extends AbstractAssert<ListOfSpansAssert, ListOfS
private String spansToString() {
return this.actual.spans.stream().map(span -> "\nSPAN: " + span.toString() + " with name [" + span.getName() + "] " +
"\nwith tags " + span.tags() + "\nwith logs " + span.logs()).collect(joining("\n"));
"\nwith tags " + span.tags() + "\nwith logs " + span.logs() +
"\nwith baggage " + span.getBaggage()).collect(joining("\n"));
}
public ListOfSpansAssert doesNotHaveASpanWithName(String name) {
@@ -143,16 +194,12 @@ public class ListOfSpansAssert extends AbstractAssert<ListOfSpansAssert, ListOfS
}
private void printSpans() {
try {
log.info("Stored spans " + this.objectMapper.writeValueAsString(new ArrayList<>(this.actual.spans)));
}
catch (JsonProcessingException e) {
}
log.info("Stored spans " + spansToString());
}
@Override
protected void failWithMessage(String errorMessage, Object... arguments) {
log.error(errorMessage);
log.error(String.format(errorMessage, arguments));
super.failWithMessage(errorMessage, arguments);
}
}

View File

@@ -99,6 +99,19 @@ public class SpanAssert extends AbstractAssert<SpanAssert, Span> {
return this;
}
public SpanAssert hasBaggageItem(String baggageKey, String baggageValue) {
isNotNull();
assertThatBaggageContainsKey(baggageKey);
String foundValue = this.actual.getBaggageItem(baggageKey);
if (!foundValue.equals(baggageValue)) {
String message = String.format("Expected span to have the baggage with key <%s> and value <%s>. "
+ "Found value for that baggage is <%s>", baggageKey, baggageValue, foundValue);
log.error(message);
failWithMessage(message);
}
return this;
}
public SpanAssert hasATagWithKey(String tagKey) {
isNotNull();
assertThatTagIsPresent(tagKey);
@@ -134,6 +147,15 @@ public class SpanAssert extends AbstractAssert<SpanAssert, Span> {
}
}
private void assertThatBaggageContainsKey(String baggageKey) {
if (!this.actual.getBaggage().containsKey(baggageKey)) {
String message = String.format("Expected span to have the baggage with key <%s>. "
+ "Found baggage are <%s>", baggageKey, this.actual.getBaggage());
log.error(message);
failWithMessage(message);
}
}
public SpanAssert hasLoggedAnEvent(String event) {
isNotNull();
if (!this.actual.logs().stream().map(org.springframework.cloud.sleuth.Log::getEvent)

View File

@@ -1,51 +1,60 @@
package org.springframework.cloud.sleuth.instrument.web.multiple;
import java.net.URI;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.sleuth.Sampler;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanReporter;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator;
import org.springframework.cloud.sleuth.TraceKeys;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.assertions.ListOfSpans;
import org.springframework.cloud.sleuth.instrument.web.TraceFilter;
import org.springframework.cloud.sleuth.instrument.web.common.AbstractMvcIntegrationTest;
import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
import org.springframework.cloud.sleuth.util.ArrayListSpanAccumulator;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.web.client.RestTemplate;
import static com.jayway.awaitility.Awaitility.await;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.cloud.sleuth.assertions.SleuthAssertions.then;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MultipleHopsIntegrationTests.Config.class)
public class MultipleHopsIntegrationTests extends AbstractMvcIntegrationTest {
@SpringBootTest(classes = MultipleHopsIntegrationTests.Config.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MultipleHopsIntegrationTests {
@Autowired Tracer tracer;
@Autowired TraceKeys traceKeys;
@Autowired TraceFilter traceFilter;
@Autowired ArrayListSpanAccumulator arrayListSpanAccumulator;
@Autowired SpanReporter spanReporter;
@Autowired RestTemplate restTemplate;
@Autowired Config config;
@Override
protected void configureMockMvcBuilder(DefaultMockMvcBuilder mockMvcBuilder) {
mockMvcBuilder.addFilters(this.traceFilter);
@Before
public void setup() {
this.arrayListSpanAccumulator.clear();
}
@Test
public void should_prepare_spans_for_export() throws Exception {
this.mockMvc.perform(get("/greeting")).andExpect(
MockMvcResultMatchers.status().isOk());
this.restTemplate.getForObject("http://localhost:" + this.config.port + "/greeting", String.class);
await().atMost(5, SECONDS).until(() -> {
then(this.arrayListSpanAccumulator.getSpans().stream().map(Span::getName)
@@ -55,9 +64,46 @@ public class MultipleHopsIntegrationTests extends AbstractMvcIntegrationTest {
});
}
// issue #237 - baggage
@Test
public void should_propagate_the_baggage() throws Exception {
//tag::baggage[]
Span initialSpan = this.tracer.createSpan("span");
initialSpan.setBaggageItem("foo", "bar");
//end::baggage[]
try {
HttpHeaders headers = new HttpHeaders();
headers.put("baggage-baz", Collections.singletonList("baz"));
RequestEntity requestEntity = new RequestEntity(headers, HttpMethod.GET,
URI.create("http://localhost:" + this.config.port + "/greeting"));
this.restTemplate.exchange(requestEntity, String.class);
await().atMost(5, SECONDS).until(() -> {
then(new ListOfSpans(this.arrayListSpanAccumulator.getSpans()))
.everySpanHasABaggage("foo", "bar")
.anySpanHasABaggage("baz", "baz");
});
} finally {
this.tracer.close(initialSpan);
}
}
@Configuration
@SpringBootApplication
public static class Config {
public static class Config implements
ApplicationListener<EmbeddedServletContainerInitializedEvent> {
int port;
@Override
public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
this.port = event.getEmbeddedServletContainer().getPort();
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean ArrayListSpanAccumulator arrayListSpanAccumulator() {
return new ArrayListSpanAccumulator();

View File

@@ -185,6 +185,38 @@ public class DefaultTracerTests {
then(span).isEqualTo(continuedSpan);
}
@Test
public void shouldPropagateBaggageFromParentToChild() {
DefaultTracer tracer = new DefaultTracer(new AlwaysSampler(), new Random(),
this.spanNamer, this.spanLogger, this.spanReporter);
Span parent = Span.builder().name(IMPORTANT_WORK_1).traceId(1L).spanId(1L)
.baggage("foo", "bar").build();
Span child = tracer.createSpan("child", parent);
then(parent).hasBaggageItem("foo", "bar");
then(child).hasBaggageItem("foo", "bar");
}
@Test
public void shouldPropagateBaggageToContinuedSpan() {
DefaultTracer tracer = new DefaultTracer(new AlwaysSampler(), new Random(),
this.spanNamer, this.spanLogger, this.spanReporter);
Span parent = Span.builder().name(IMPORTANT_WORK_1).traceId(1L).spanId(1L)
.baggage("foo", "bar").build();
Span continuedSpan = tracer.continueSpan(parent);
parent.setBaggageItem("baz1", "baz1");
continuedSpan.setBaggageItem("baz2", "baz2");
then(parent).hasBaggageItem("foo", "bar")
.hasBaggageItem("baz1", "baz1")
.hasBaggageItem("baz2", "baz2");
then(continuedSpan).hasBaggageItem("foo", "bar")
.hasBaggageItem("baz1", "baz1")
.hasBaggageItem("baz2", "baz2");
then(parent).isEqualTo(continuedSpan);
}
private Span assertSpan(List<Span> spans, Long parentId, String name) {
List<Span> found = findSpans(spans, parentId);
assertThat(found).as("More than one span with parentId %s", parentId).hasSize(1);