Initial pass at TraceInterceptor API
ADD
dd-trace-api/src/main/java/datadog/trace/api/interceptor/MutableSpan.java
+package datadog.trace.api.interceptor;
+
+import java.util.Map;
+
+public interface MutableSpan {
+ String getOperationName();
+
+ MutableSpan setOperationName(final String serviceName);
+
+ String getServiceName();
+
+ MutableSpan setServiceName(final String serviceName);
+
+ String getResourceName();
+
+ MutableSpan setResourceName(final String resourceName);
+
+ Integer getSamplingPriority();
+
+ MutableSpan setSamplingPriority(final int newPriority);
+
+ String getSpanType();
+
+ MutableSpan setSpanType(final String type);
+
+ Map<String, Object> getTags();
+
+ MutableSpan setTag(final String tag, final String value);
+
+ MutableSpan setTag(final String tag, final boolean value);
+
+ MutableSpan setTag(final String tag, final Number value);
+
+ Boolean isError();
+
+ MutableSpan setError(boolean value);
+}

ADD
dd-trace-api/src/main/java/datadog/trace/api/interceptor/TraceInterceptor.java
+package datadog.trace.api.interceptor;
+
+import java.util.Collection;
+
+public interface TraceInterceptor {
+
+ /**
+ * After a trace is "complete" but before it is written, it is provided to the interceptors to
+ * modify. The result following all interceptors is sampled then sent to the trace writer.
+ *
+ * @param trace - The collection of spans that represent a trace. Can be modified in place. Order
+ * of spans should not be relied upon.
+ * @return A potentially modified or replaced collection of spans. Must not be null.
+ */
+ Collection<? extends MutableSpan> onTraceComplete(Collection<? extends MutableSpan> trace);
+
+ /**
+ * @return A unique priority for sorting relative to other TraceInterceptors. Unique because
+ * interceptors are stored in a sorted set, so duplicates will not be added.
+ */
+ int priority();
+}

MODIFY
dd-trace-ot/src/main/java/datadog/opentracing/DDSpan.java
dd-trace-ot/src/main/java/datadog/opentracing/DDSpan.java
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import datadog.trace.api.DDTags;
+import datadog.trace.api.interceptor.MutableSpan;
import datadog.trace.common.sampling.PrioritySampling;
import datadog.trace.common.util.Clock;
import io.opentracing.Span;
@@ -26,7 +27,7 @@ import lombok.extern.slf4j.Slf4j;
* according to the DD agent.
*/
@Slf4j
-public class DDSpan implements Span {
+public class DDSpan implements Span, MutableSpan {
/** The context attached to the span */
private final DDSpanContext context;
@@ -109,8 +110,14 @@ public class DDSpan implements Span {
return context.getParentId() == 0;
}
- public void setErrorMeta(final Throwable error) {
+ @Override
+ public DDSpan setError(final boolean error) {
context.setErrorFlag(true);
+ return this;
+ }
+
+ public void setErrorMeta(final Throwable error) {
+ setError(true);
setTag(DDTags.ERROR_MSG, error.getMessage());
setTag(DDTags.ERROR_TYPE, error.getClass().getName());
@@ -133,7 +140,7 @@ public class DDSpan implements Span {
* @see io.opentracing.BaseSpan#setTag(java.lang.String, java.lang.String)
*/
@Override
- public final Span setTag(final String tag, final String value) {
+ public final DDSpan setTag(final String tag, final String value) {
this.context().setTag(tag, (Object) value);
return this;
}
@@ -142,7 +149,7 @@ public class DDSpan implements Span {
* @see io.opentracing.BaseSpan#setTag(java.lang.String, boolean)
*/
@Override
- public final Span setTag(final String tag, final boolean value) {
+ public final DDSpan setTag(final String tag, final boolean value) {
this.context().setTag(tag, (Object) value);
return this;
}
@@ -151,7 +158,7 @@ public class DDSpan implements Span {
* @see io.opentracing.BaseSpan#setTag(java.lang.String, java.lang.Number)
*/
@Override
- public final Span setTag(final String tag, final Number value) {
+ public final DDSpan setTag(final String tag, final Number value) {
this.context().setTag(tag, (Object) value);
return this;
}
@@ -230,11 +237,13 @@ public class DDSpan implements Span {
return this;
}
+ @Override
public final DDSpan setServiceName(final String serviceName) {
this.context().setServiceName(serviceName);
return this;
}
+ @Override
public final DDSpan setResourceName(final String resourceName) {
this.context().setResourceName(resourceName);
return this;
@@ -245,11 +254,13 @@ public class DDSpan implements Span {
*
* <p>Has no effect if the span priority has been propagated (injected or extracted).
*/
+ @Override
public final DDSpan setSamplingPriority(final int newPriority) {
this.context().setSamplingPriority(newPriority);
return this;
}
+ @Override
public final DDSpan setSpanType(final String type) {
this.context().setSpanType(type);
return this;
@@ -284,6 +295,7 @@ public class DDSpan implements Span {
return durationNano.get();
}
+ @Override
@JsonGetter("service")
public String getServiceName() {
return context.getServiceName();
@@ -304,16 +316,19 @@ public class DDSpan implements Span {
return context.getParentId();
}
+ @Override
@JsonGetter("resource")
public String getResourceName() {
return context.getResourceName();
}
+ @Override
@JsonGetter("name")
public String getOperationName() {
return context.getOperationName();
}
+ @Override
@JsonGetter("sampling_priority")
@JsonInclude(Include.NON_NULL)
public Integer getSamplingPriority() {
@@ -325,6 +340,13 @@ public class DDSpan implements Span {
}
}
+ @Override
+ @JsonIgnore
+ public String getSpanType() {
+ return context.getSpanType();
+ }
+
+ @Override
@JsonIgnore
public Map<String, Object> getTags() {
return this.context().getTags();
@@ -335,6 +357,12 @@ public class DDSpan implements Span {
return context.getSpanType();
}
+ @Override
+ @JsonIgnore
+ public Boolean isError() {
+ return context.getErrorFlag();
+ }
+
@JsonGetter
public int getError() {
return context.getErrorFlag() ? 1 : 0;

MODIFY
dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java
dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java
package datadog.opentracing;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import datadog.opentracing.decorators.AbstractDecorator;
import datadog.opentracing.decorators.DDDecoratorsFactory;
@@ -10,6 +11,8 @@ import datadog.opentracing.propagation.HTTPCodec;
import datadog.opentracing.scopemanager.ContextualScopeManager;
import datadog.opentracing.scopemanager.ScopeContext;
import datadog.trace.api.DDTags;
+import datadog.trace.api.interceptor.MutableSpan;
+import datadog.trace.api.interceptor.TraceInterceptor;
import datadog.trace.common.DDTraceConfig;
import datadog.trace.common.Service;
import datadog.trace.common.sampling.AllSampler;
@@ -25,12 +28,17 @@ import io.opentracing.Span;
import io.opentracing.SpanContext;
import io.opentracing.propagation.Format;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.ServiceLoader;
+import java.util.SortedSet;
+import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ThreadLocalRandom;
import lombok.extern.slf4j.Slf4j;
@@ -56,6 +64,14 @@ public class DDTracer implements io.opentracing.Tracer {
private final Map<String, List<AbstractDecorator>> spanContextDecorators =
new ConcurrentHashMap<>();
+ private final SortedSet<TraceInterceptor> interceptors =
+ new ConcurrentSkipListSet<>(
+ new Comparator<TraceInterceptor>() {
+ @Override
+ public int compare(final TraceInterceptor o1, final TraceInterceptor o2) {
+ return Integer.compare(o1.priority(), o2.priority());
+ }
+ });
private final CodecRegistry registry;
private final Map<String, Service> services = new HashMap<>();
@@ -106,6 +122,10 @@ public class DDTracer implements io.opentracing.Tracer {
final DDApi api = ((DDAgentWriter) this.writer).getApi();
api.addResponseListener((DDApi.ResponseListener) this.sampler);
}
+
+ for (final TraceInterceptor interceptor : ServiceLoader.load(TraceInterceptor.class)) {
+ interceptors.add(interceptor);
+ }
log.info("New instance: {}", this);
}
@@ -142,6 +162,17 @@ public class DDTracer implements io.opentracing.Tracer {
spanContextDecorators.put(decorator.getMatchingTag(), list);
}
+ /**
+ * Add a new interceptor to the tracer. Interceptors with duplicate priority to existing ones are
+ * ignored.
+ *
+ * @param interceptor
+ * @return false if an interceptor with same priority exists.
+ */
+ public boolean addInterceptor(final TraceInterceptor interceptor) {
+ return interceptors.add(interceptor);
+ }
+
public void addScopeContext(final ScopeContext context) {
scopeManager.addScopeContext(context);
}
@@ -194,8 +225,23 @@ public class DDTracer implements io.opentracing.Tracer {
if (trace.isEmpty()) {
return;
}
- if (this.sampler.sample(trace.peek())) {
- this.writer.write(new ArrayList<>(trace));
+ final ArrayList<DDSpan> writtenTrace;
+ if (interceptors.isEmpty()) {
+ writtenTrace = Lists.newArrayList(trace);
+ } else {
+ Collection<? extends MutableSpan> interceptedTrace = Lists.newArrayList(trace);
+ for (final TraceInterceptor interceptor : interceptors) {
+ interceptedTrace = interceptor.onTraceComplete(interceptedTrace);
+ }
+ writtenTrace = Lists.newArrayListWithExpectedSize(interceptedTrace.size());
+ for (final MutableSpan span : interceptedTrace) {
+ if (span instanceof DDSpan) {
+ writtenTrace.add((DDSpan) span);
+ }
+ }
+ }
+ if (!writtenTrace.isEmpty() && this.sampler.sample(writtenTrace.get(0))) {
+ this.writer.write(writtenTrace);
}
}

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanBuilderTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanBuilderTest.groovy
@@ -235,6 +235,9 @@ class DDSpanBuilderTest extends Specification {
expect:
span.tags == tags
+ cleanup:
+ System.clearProperty("dd.trace.span.tags")
+
where:
tagString | tags
"" | [:]

MODIFY
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanSerializationTest.groovy
dd-trace-ot/src/test/groovy/datadog/opentracing/DDSpanSerializationTest.groovy
@@ -9,7 +9,7 @@ import spock.lang.Specification
import spock.lang.Timeout
import spock.lang.Unroll
-@Timeout(2)
+@Timeout(5)
class DDSpanSerializationTest extends Specification {
@Unroll

ADD
dd-trace-ot/src/test/groovy/datadog/opentracing/TraceInterceptorTest.groovy
+package datadog.opentracing
+
+import datadog.trace.api.interceptor.MutableSpan
+import datadog.trace.api.interceptor.TraceInterceptor
+import datadog.trace.common.writer.ListWriter
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+class TraceInterceptorTest extends Specification {
+ def writer = new ListWriter()
+ def tracer = new DDTracer(writer)
+
+ def "interceptor is registered as a service"() {
+ expect:
+ tracer.interceptors.iterator().hasNext()
+ tracer.interceptors.iterator().next() instanceof TestInterceptor
+ }
+
+ def "interceptors with the same priority replaced"() {
+ setup:
+ int priority = 999
+ ((TestInterceptor) tracer.interceptors.iterator().next()).priority = priority
+ tracer.interceptors.add(new TraceInterceptor() {
+ @Override
+ Collection<? extends MutableSpan> onTraceComplete(Collection<? extends MutableSpan> trace) {
+ return null
+ }
+
+ @Override
+ int priority() {
+ return priority
+ }
+ })
+
+ expect:
+ tracer.interceptors.size() == 1
+ tracer.interceptors.iterator().next() instanceof TestInterceptor
+ }
+
+ def "interceptors with different priority sorted"() {
+ setup:
+ def priority = score
+ def existingInterceptor = tracer.interceptors.iterator().next()
+ def newInterceptor = new TraceInterceptor() {
+ @Override
+ Collection<? extends MutableSpan> onTraceComplete(Collection<? extends MutableSpan> trace) {
+ return null
+ }
+
+ @Override
+ int priority() {
+ return priority
+ }
+ }
+ tracer.interceptors.add(newInterceptor)
+
+ expect:
+ tracer.interceptors == reverse ? [newInterceptor, existingInterceptor] : [existingInterceptor, newInterceptor]
+
+ where:
+ score | reverse
+ -1 | false
+ 1 | true
+ }
+
+ @Unroll
+ def "interceptor can discard a trace (p=#score)"() {
+ setup:
+ def called = new AtomicBoolean(false)
+ def priority = score
+ tracer.interceptors.add(new TraceInterceptor() {
+ @Override
+ Collection<? extends MutableSpan> onTraceComplete(Collection<? extends MutableSpan> trace) {
+ called.set(true)
+ return Collections.emptyList()
+ }
+
+ @Override
+ int priority() {
+ return priority
+ }
+ })
+ tracer.buildSpan("test").start().finish()
+
+ expect:
+ tracer.interceptors.size() == Math.abs(score) + 1
+ (called.get()) == (score != 0)
+ (writer == []) == (score != 0)
+
+ where:
+ score | _
+ -1 | _
+ 0 | _
+ 1 | _
+ }
+
+ @Unroll
+ def "interceptor can modify a span"() {
+ setup:
+ tracer.interceptors.add(new TraceInterceptor() {
+ @Override
+ Collection<? extends MutableSpan> onTraceComplete(Collection<? extends MutableSpan> trace) {
+ for (MutableSpan span : trace) {
+ span
+ .setOperationName("modifiedON-" + span.getOperationName())
+ .setServiceName("modifiedSN-" + span.getServiceName())
+ .setResourceName("modifiedRN-" + span.getResourceName())
+ .setSpanType("modifiedST-" + span.getSpanType())
+ .setTag("boolean-tag", true)
+ .setTag("number-tag", 5.0)
+ .setTag("string-tag", "howdy")
+ .setError(true)
+ }
+ return trace
+ }
+
+ @Override
+ int priority() {
+ return 1
+ }
+ })
+ tracer.buildSpan("test").start().finish()
+
+ expect:
+ def trace = writer.firstTrace()
+ trace.size() == 1
+
+ def span = trace[0]
+
+ span.context().operationName == "modifiedON-test"
+ span.serviceName == "modifiedSN-unnamed-java-app"
+ span.resourceName == "modifiedRN-modifiedON-test"
+ span.type == "modifiedST-null"
+ span.context().getErrorFlag()
+
+ def tags = span.context().tags
+
+ tags["boolean-tag"] == true
+ tags["number-tag"] == 5.0
+ tags["string-tag"] == "howdy"
+
+ tags["span.type"] == "modifiedST-null"
+ tags["thread.name"] != null
+ tags["thread.id"] != null
+ tags.size() == 6
+ }
+}

ADD
dd-trace-ot/src/test/java/datadog/opentracing/TestInterceptor.java
+package datadog.opentracing;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.api.interceptor.MutableSpan;
+import datadog.trace.api.interceptor.TraceInterceptor;
+import java.util.Collection;
+
+@AutoService(TraceInterceptor.class)
+public class TestInterceptor implements TraceInterceptor {
+ public volatile int priority = 0;
+
+ @Override
+ public Collection<? extends MutableSpan> onTraceComplete(
+ final Collection<? extends MutableSpan> trace) {
+ return trace;
+ }
+
+ @Override
+ public int priority() {
+ return priority;
+ }
+}

Initial pass at TraceInterceptor API