Configure tags to be added to every span

This commit is contained in:
Tyler Benson 2018-03-01 14:31:03 +10:00
parent b3c8c3fd6b
commit 759fb4e815
7 changed files with 328 additions and 237 deletions

View File

@ -148,7 +148,7 @@ public class DDSpanContext implements io.opentracing.SpanContext {
this.spanType = spanType;
}
public void setSamplingPriority(int newPriority) {
public void setSamplingPriority(final int newPriority) {
if (samplingPriorityLocked) {
log.warn(
"samplingPriority locked at {}. Refusing to set to {}", samplingPriority, newPriority);
@ -272,7 +272,7 @@ public class DDSpanContext implements io.opentracing.SpanContext {
public synchronized Map<String, Object> getTags() {
if (tags.isEmpty()) {
tags = Maps.newHashMapWithExpectedSize(2);
tags = Maps.newHashMapWithExpectedSize(3);
}
tags.put(DDTags.THREAD_NAME, threadName);
tags.put(DDTags.THREAD_ID, threadId);

View File

@ -1,6 +1,7 @@
package datadog.opentracing;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.Maps;
import datadog.opentracing.decorators.AbstractDecorator;
import datadog.opentracing.decorators.DDDecoratorsFactory;
import datadog.opentracing.propagation.Codec;
@ -44,6 +45,9 @@ public class DDTracer extends ThreadLocalScopeManager implements io.opentracing.
/** Sampler defines the sampling policy in order to reduce the number of traces for instance */
final Sampler sampler;
/** A set of tags that are added to every span */
private final Map<String, Object> spanTags;
/** Span context decorators */
private final Map<String, List<AbstractDecorator>> spanContextDecorators = new HashMap<>();
@ -63,7 +67,8 @@ public class DDTracer extends ThreadLocalScopeManager implements io.opentracing.
this(
config.getProperty(DDTraceConfig.SERVICE_NAME),
Writer.Builder.forConfig(config),
Sampler.Builder.forConfig(config));
Sampler.Builder.forConfig(config),
DDTraceConfig.parseMap(config.getProperty(DDTraceConfig.SPAN_TAGS)));
log.debug("Using config: {}", config);
// Create decorators from resource files
@ -75,10 +80,20 @@ public class DDTracer extends ThreadLocalScopeManager implements io.opentracing.
}
public DDTracer(final String serviceName, final Writer writer, final Sampler sampler) {
this(serviceName, writer, sampler, Collections.<String, Object>emptyMap());
}
public DDTracer(
final String serviceName,
final Writer writer,
final Sampler sampler,
final Map<String, Object> spanTags) {
this.serviceName = serviceName;
this.writer = writer;
this.writer.start();
this.sampler = sampler;
this.spanTags = spanTags;
registry = new CodecRegistry();
registry.register(Format.Builtin.HTTP_HEADERS, new HTTPCodec());
registry.register(Format.Builtin.TEXT_MAP, new HTTPCodec());
@ -90,7 +105,11 @@ public class DDTracer extends ThreadLocalScopeManager implements io.opentracing.
}
public DDTracer(final Writer writer) {
this(UNASSIGNED_DEFAULT_SERVICE_NAME, writer, new AllSampler());
this(
UNASSIGNED_DEFAULT_SERVICE_NAME,
writer,
new AllSampler(),
DDTraceConfig.parseMap(new DDTraceConfig().getProperty(DDTraceConfig.SPAN_TAGS)));
}
/**
@ -185,6 +204,8 @@ public class DDTracer extends ThreadLocalScopeManager implements io.opentracing.
+ writer
+ ", sampler="
+ sampler
+ ", tags="
+ spanTags
+ '}';
}
@ -236,7 +257,8 @@ public class DDTracer extends ThreadLocalScopeManager implements io.opentracing.
private final String operationName;
// Builder attributes
private Map<String, Object> tags = Collections.emptyMap();
private Map<String, Object> tags =
spanTags.isEmpty() ? Collections.<String, Object>emptyMap() : Maps.newHashMap(spanTags);
private long timestamp;
private SpanContext parent;
private String serviceName;
@ -361,7 +383,7 @@ public class DDTracer extends ThreadLocalScopeManager implements io.opentracing.
// Private methods
private DDSpanBuilder withTag(final String tag, final Object value) {
if (this.tags.isEmpty()) {
this.tags = new HashMap<>();
this.tags = Maps.newHashMap();
}
this.tags.put(tag, value);
return this;

View File

@ -1,9 +1,13 @@
package datadog.trace.common;
import com.google.common.collect.Maps;
import datadog.opentracing.DDTracer;
import datadog.trace.common.writer.DDAgentWriter;
import datadog.trace.common.writer.Writer;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
/**
* Config gives priority to system properties and falls back to environment variables. It also
@ -14,6 +18,7 @@ import java.util.Properties;
* <p>System properties are {@link DDTraceConfig#PREFIX}'ed. Environment variables are the same as
* the system property, but uppercased with '.' -> '_'.
*/
@Slf4j
public class DDTraceConfig extends Properties {
/** Config keys below */
private static final String PREFIX = "dd.";
@ -23,12 +28,14 @@ public class DDTraceConfig extends Properties {
public static final String AGENT_HOST = "agent.host";
public static final String AGENT_PORT = "agent.port";
public static final String PRIORITY_SAMPLING = "priority.sampling";
public static final String SPAN_TAGS = "trace.span.tags";
private final String serviceName = getPropOrEnv(PREFIX + SERVICE_NAME);
private final String writerType = getPropOrEnv(PREFIX + WRITER_TYPE);
private final String agentHost = getPropOrEnv(PREFIX + AGENT_HOST);
private final String agentPort = getPropOrEnv(PREFIX + AGENT_PORT);
private final String prioritySampling = getPropOrEnv(PREFIX + PRIORITY_SAMPLING);
private final String spanTags = getPropOrEnv(PREFIX + SPAN_TAGS);
public DDTraceConfig() {
super();
@ -45,6 +52,7 @@ public class DDTraceConfig extends Properties {
setIfNotNull(AGENT_HOST, agentHost);
setIfNotNull(AGENT_PORT, agentPort);
setIfNotNull(PRIORITY_SAMPLING, prioritySampling);
setIfNotNull(SPAN_TAGS, spanTags);
}
public DDTraceConfig(final String serviceName) {
@ -65,4 +73,23 @@ public class DDTraceConfig extends Properties {
static String propToEnvName(final String name) {
return name.toUpperCase().replace(".", "_");
}
public static Map<String, Object> parseMap(final String str) {
if (str == null || str.trim().isEmpty()) {
return Collections.emptyMap();
}
if (!str.matches("(([^,:]+:[^,:]+,)*([^,:]+:[^,:]+),?)?")) {
log.warn("Invalid config '{}'. Must match 'key1:value1,key2:value2'.", str);
return Collections.emptyMap();
}
final String[] tokens = str.split(",");
final Map<String, Object> map = Maps.newHashMapWithExpectedSize(tokens.length);
for (final String token : tokens) {
final String[] keyValue = token.split(":");
map.put(keyValue[0].trim(), keyValue[1].trim());
}
return Collections.unmodifiableMap(map);
}
}

View File

@ -0,0 +1,241 @@
package datadog.opentracing
import datadog.trace.api.DDTags
import datadog.trace.common.writer.ListWriter
import spock.lang.Specification
import static java.util.concurrent.TimeUnit.MILLISECONDS
import static org.mockito.Mockito.mock
import static org.mockito.Mockito.when
class DDSpanBuilderTest extends Specification {
def writer = new ListWriter()
def tracer = new DDTracer(writer)
def "build simple span"() {
setup:
final DDSpan span = tracer.buildSpan("op name").withServiceName("foo").start()
expect:
span.operationName == "op name"
}
def "build complex span"() {
setup:
def expectedName = "fakeName"
def tags = [
"1": true,
"2": "fakeString",
"3": 42.0,
]
DDTracer.DDSpanBuilder builder = tracer
.buildSpan(expectedName)
.withServiceName("foo")
tags.each {
builder = builder.withTag(it.key, it.value)
}
when:
DDSpan span = builder.start()
then:
span.getOperationName() == expectedName
span.tags.subMap(tags.keySet()) == tags
when:
span = tracer.buildSpan(expectedName).withServiceName("foo").start()
then:
span.getTags() == [(DDTags.THREAD_NAME): Thread.currentThread().getName(),
(DDTags.THREAD_ID) : Thread.currentThread().getId(),
(DDTags.SPAN_TYPE) : null]
when:
// with all custom fields provided
final String expectedResource = "fakeResource"
final String expectedService = "fakeService"
final String expectedType = "fakeType"
span =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.withResourceName(expectedResource)
.withServiceName(expectedService)
.withErrorFlag()
.withSpanType(expectedType)
.start()
final DDSpanContext context = span.context()
then:
context.getResourceName() == expectedResource
context.getErrorFlag()
context.getServiceName() == expectedService
context.getSpanType() == expectedType
context.tags[DDTags.THREAD_NAME] == Thread.currentThread().getName()
context.tags[DDTags.THREAD_ID] == Thread.currentThread().getId()
}
def "should build span timestamp in nano"() {
setup:
// time in micro
final long expectedTimestamp = 487517802L * 1000 * 1000L
final String expectedName = "fakeName"
DDSpan span =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.withStartTimestamp(expectedTimestamp)
.start()
expect:
// get return nano time
span.getStartTime() == expectedTimestamp * 1000L
when:
// auto-timestamp in nanoseconds
def start = System.currentTimeMillis()
span = tracer.buildSpan(expectedName).withServiceName("foo").start()
def stop = System.currentTimeMillis()
then:
// Give a range of +/- 5 millis
span.getStartTime() >= MILLISECONDS.toNanos(start - 1)
span.getStartTime() <= MILLISECONDS.toNanos(stop + 1)
}
def "should link to parent span"() {
setup:
final long spanId = 1L
final long expectedParentId = spanId
final DDSpanContext mockedContext = mock(DDSpanContext)
when(mockedContext.getSpanId()).thenReturn(spanId)
when(mockedContext.getServiceName()).thenReturn("foo")
final String expectedName = "fakeName"
final DDSpan span =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.asChildOf(mockedContext)
.start()
final DDSpanContext actualContext = span.context()
expect:
actualContext.getParentId() == expectedParentId
}
def "should inherit the DD parent attributes"() {
setup:
def expectedName = "fakeName"
def expectedParentServiceName = "fakeServiceName"
def expectedParentResourceName = "fakeResourceName"
def expectedParentType = "fakeType"
def expectedChildServiceName = "fakeServiceName-child"
def expectedChildResourceName = "fakeResourceName-child"
def expectedChildType = "fakeType-child"
def expectedBaggageItemKey = "fakeKey"
def expectedBaggageItemValue = "fakeValue"
final DDSpan parent =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.withResourceName(expectedParentResourceName)
.withSpanType(expectedParentType)
.start()
parent.setBaggageItem(expectedBaggageItemKey, expectedBaggageItemValue)
// ServiceName and SpanType are always set by the parent if they are not present in the child
DDSpan span =
tracer
.buildSpan(expectedName)
.withServiceName(expectedParentServiceName)
.asChildOf(parent)
.start()
expect:
span.getOperationName() == expectedName
span.getBaggageItem(expectedBaggageItemKey) == expectedBaggageItemValue
span.context().getServiceName() == expectedParentServiceName
span.context().getResourceName() == expectedName
span.context().getSpanType() == expectedParentType
when:
// ServiceName and SpanType are always overwritten by the child if they are present
span =
tracer
.buildSpan(expectedName)
.withServiceName(expectedChildServiceName)
.withResourceName(expectedChildResourceName)
.withSpanType(expectedChildType)
.asChildOf(parent)
.start()
then:
span.getOperationName() == expectedName
span.getBaggageItem(expectedBaggageItemKey) == expectedBaggageItemValue
span.context().getServiceName() == expectedChildServiceName
span.context().getResourceName() == expectedChildResourceName
span.context().getSpanType() == expectedChildType
}
def "should track all spans in trace"() {
setup:
List<DDSpan> spans = []
final int nbSamples = 10
// root (aka spans[0]) is the parent
// others are just for fun
def root = tracer.buildSpan("fake_O").withServiceName("foo").start()
spans.add(root)
final long tickEnd = System.currentTimeMillis()
for (int i = 1; i <= 10; i++) {
spans.add(tracer
.buildSpan("fake_" + i)
.withServiceName("foo")
.asChildOf(spans.get(i - 1))
.start())
}
spans.get(1).finish(tickEnd)
expect:
root.context().getTrace().size() == nbSamples + 1
root.context().getTrace().containsAll(spans)
spans[(int) (Math.random() * nbSamples)].context.trace.containsAll(spans)
}
def "global span tags populated on each span"() {
setup:
System.setProperty("dd.trace.span.tags", tagString)
tracer = new DDTracer(writer)
def span = tracer.buildSpan("op name").withServiceName("foo").start()
tags.putAll([(DDTags.THREAD_NAME): Thread.currentThread().getName(),
(DDTags.THREAD_ID) : Thread.currentThread().getId(),
(DDTags.SPAN_TYPE) : null])
expect:
span.tags == tags
where:
tagString | tags
"" | [:]
"in:val:id" | [:]
"a:x" | [a: "x"]
"a:a,a:b,a:c" | [a: "c"]
"a:1,b-c:d" | [a: "1", "b-c": "d"]
}
}

View File

@ -120,7 +120,7 @@ class DDSpanTest extends Specification {
def between = System.currentTimeMillis()
def betweenDur = System.currentTimeMillis() - between
span.finish()
def total = System.currentTimeMillis() - start
def total = Math.max(1, System.currentTimeMillis() - start)
expect:
span.durationNano >= TimeUnit.MILLISECONDS.toNanos(betweenDur)

View File

@ -118,4 +118,35 @@ class DDTraceConfigTest extends Specification {
"writer" | "agent.host" | "somethingelse" | "DDAgentWriter { api=DDApi { tracesEndpoint=http://somethingelse:8126/v0.3/traces } }"
"writer" | "agent.port" | "9999" | "DDAgentWriter { api=DDApi { tracesEndpoint=http://localhost:9999/v0.3/traces } }"
}
def "parsing valid string returns a map"() {
expect:
DDTraceConfig.parseMap(str) == map
where:
str | map
"a:a;" | [a: "a;"]
"a:1, a:2, a:3" | [a: "3"]
"a:b,c:d" | [a: "b", c: "d"]
"key 1!:va|ue_1," | ["key 1!": "va|ue_1"]
" key1 :value1 ,\t key2: value2" | [key1: "value1", key2: "value2"]
}
def "parsing an invalid string returns an empty map"() {
expect:
DDTraceConfig.parseMap(str) == map
where:
str | map
null | [:]
"" | [:]
"1" | [:]
"a" | [:]
"a:" | [:]
"a,1" | [:]
"in:val:id" | [:]
"a:b:c:d" | [:]
"a:b,c,d" | [:]
"!a" | [:]
}
}

View File

@ -1,230 +0,0 @@
package datadog.opentracing;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import datadog.trace.api.DDTags;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class DDSpanBuilderTest {
private DDTracer tracer;
@Before
public void setUp() throws Exception {
tracer = new DDTracer();
}
@After
public void tearDown() throws Exception {}
@Test
public void shouldBuildSimpleSpan() {
final String expectedName = "fakeName";
final DDSpan span = tracer.buildSpan(expectedName).withServiceName("foo").startManual();
assertThat(span.getOperationName()).isEqualTo(expectedName);
}
@Test
public void shouldBuildMoreComplexSpan() {
final String expectedName = "fakeName";
final Map tags =
new HashMap<String, Object>() {
{
put("1", true);
put("2", "fakeString");
put("3", 42.0);
}
};
DDSpan span =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.withTag("1", (Boolean) tags.get("1"))
.withTag("2", (String) tags.get("2"))
.withTag("3", (Number) tags.get("3"))
.startManual();
assertThat(span.getOperationName()).isEqualTo(expectedName);
assertThat(span.getTags()).containsAllEntriesOf(tags);
// with no tag provided
span = tracer.buildSpan(expectedName).withServiceName("foo").startManual();
assertThat(span.getTags()).isNotNull();
assertThat(span.getTags().size()).isEqualTo(3);
// with all custom fields provided
final String expectedResource = "fakeResource";
final String expectedService = "fakeService";
final String expectedType = "fakeType";
span =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.withResourceName(expectedResource)
.withServiceName(expectedService)
.withErrorFlag()
.withSpanType(expectedType)
.startManual();
final DDSpanContext actualContext = span.context();
assertThat(actualContext.getResourceName()).isEqualTo(expectedResource);
assertThat(actualContext.getErrorFlag()).isTrue();
assertThat(actualContext.getServiceName()).isEqualTo(expectedService);
assertThat(actualContext.getSpanType()).isEqualTo(expectedType);
assertThat(actualContext.getTags().get(DDTags.THREAD_NAME))
.isEqualTo(Thread.currentThread().getName());
assertThat(actualContext.getTags().get(DDTags.THREAD_ID))
.isEqualTo(Thread.currentThread().getId());
}
@Test
public void shouldBuildSpanTimestampInNano() {
// time in micro
final long expectedTimestamp = 487517802L * 1000 * 1000L;
final String expectedName = "fakeName";
DDSpan span =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.withStartTimestamp(expectedTimestamp)
.start();
// get return nano time
assertThat(span.getStartTime()).isEqualTo(expectedTimestamp * 1000L);
// auto-timestamp in nanoseconds
final long tick = System.currentTimeMillis();
span = tracer.buildSpan(expectedName).withServiceName("foo").startManual();
// Give a range of +/- 5 millis
assertThat(span.getStartTime())
.isBetween(MILLISECONDS.toNanos(tick - 5), MILLISECONDS.toNanos(tick + 5));
}
@Test
public void shouldLinkToParentSpan() {
final long spanId = 1L;
final long expectedParentId = spanId;
final DDSpanContext mockedContext = mock(DDSpanContext.class);
when(mockedContext.getSpanId()).thenReturn(spanId);
when(mockedContext.getServiceName()).thenReturn("foo");
final String expectedName = "fakeName";
final DDSpan span =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.asChildOf(mockedContext)
.startManual();
final DDSpanContext actualContext = span.context();
assertThat(actualContext.getParentId()).isEqualTo(expectedParentId);
}
@Test
public void shouldInheritOfTheDDParentAttributes() {
final String expectedName = "fakeName";
final String expectedParentServiceName = "fakeServiceName";
final String expectedParentResourceName = "fakeResourceName";
final String expectedParentType = "fakeType";
final String expectedChildServiceName = "fakeServiceName-child";
final String expectedChildResourceName = "fakeResourceName-child";
final String expectedChildType = "fakeType-child";
final String expectedBaggageItemKey = "fakeKey";
final String expectedBaggageItemValue = "fakeValue";
final DDSpan parent =
tracer
.buildSpan(expectedName)
.withServiceName("foo")
.withResourceName(expectedParentResourceName)
.withSpanType(expectedParentType)
.startManual();
parent.setBaggageItem(expectedBaggageItemKey, expectedBaggageItemValue);
// ServiceName and SpanType are always set by the parent if they are not present in the child
DDSpan span =
tracer
.buildSpan(expectedName)
.withServiceName(expectedParentServiceName)
.asChildOf(parent)
.startManual();
assertThat(span.getOperationName()).isEqualTo(expectedName);
assertThat(span.getBaggageItem(expectedBaggageItemKey)).isEqualTo(expectedBaggageItemValue);
assertThat(span.context().getServiceName()).isEqualTo(expectedParentServiceName);
assertThat(span.context().getResourceName()).isNotEqualTo(expectedParentResourceName);
assertThat(span.context().getSpanType()).isEqualTo(expectedParentType);
// ServiceName and SpanType are always overwritten by the child if they are present
span =
tracer
.buildSpan(expectedName)
.withServiceName(expectedChildServiceName)
.withResourceName(expectedChildResourceName)
.withSpanType(expectedChildType)
.asChildOf(parent)
.startManual();
assertThat(span.getOperationName()).isEqualTo(expectedName);
assertThat(span.getBaggageItem(expectedBaggageItemKey)).isEqualTo(expectedBaggageItemValue);
assertThat(span.context().getServiceName()).isEqualTo(expectedChildServiceName);
assertThat(span.context().getResourceName()).isEqualTo(expectedChildResourceName);
assertThat(span.context().getSpanType()).isEqualTo(expectedChildType);
}
@Test
public void shouldTrackAllSpanInTrace() throws InterruptedException {
final ArrayList<DDSpan> spans = new ArrayList<>();
final int nbSamples = 10;
// root (aka spans[0]) is the parent
// others are just for fun
final DDSpan root = tracer.buildSpan("fake_O").withServiceName("foo").startManual();
spans.add(root);
Thread.sleep(200);
final long tickEnd = System.currentTimeMillis();
for (int i = 1; i <= 10; i++) {
spans.add(
tracer
.buildSpan("fake_" + i)
.withServiceName("foo")
.asChildOf(spans.get(i - 1))
.startManual());
}
spans.get(1).finish(tickEnd);
assertThat(root.context().getTrace()).hasSize(nbSamples + 1);
assertThat(root.context().getTrace()).containsAll(spans);
assertThat(spans.get((int) (Math.random() * nbSamples)).context().getTrace())
.containsAll(spans);
}
}