trace);
+
+ /**
+ * Indicates to the writer that no future writing will come and it should terminates all connections and tasks
+ */
+ void close();
+}
diff --git a/src/main/java/com/datadoghq/trace/impl/AllSampler.java b/src/main/java/com/datadoghq/trace/impl/AllSampler.java
new file mode 100644
index 00000000000..46448bc773a
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/impl/AllSampler.java
@@ -0,0 +1,16 @@
+package com.datadoghq.trace.impl;
+
+import com.datadoghq.trace.Sampler;
+import io.opentracing.Span;
+
+/**
+ * Sampler that always says yes...
+ */
+public class AllSampler implements Sampler {
+
+ @Override
+ public boolean sample(Span span) {
+ return true;
+ }
+
+}
diff --git a/src/main/java/com/datadoghq/trace/impl/DDSpan.java b/src/main/java/com/datadoghq/trace/impl/DDSpan.java
new file mode 100644
index 00000000000..3b3a3596d53
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/impl/DDSpan.java
@@ -0,0 +1,321 @@
+package com.datadoghq.trace.impl;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.opentracing.Span;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Represents an in-flight span in the opentracing system.
+ *
+ *
Spans are created by the {@link DDTracer#buildSpan}.
+ * This implementation adds some features according to the DD agent.
+ */
+public class DDSpan implements io.opentracing.Span {
+
+
+ /**
+ * StartTime stores the creation time of the span in milliseconds
+ */
+ private long startTimeMicro;
+ /**
+ * StartTimeNano stores the only the nanoseconds for more accuracy
+ */
+ private long startTimeNano;
+ /**
+ * The duration in nanoseconds computed using the startTimeMicro and startTimeNano
+ */
+ private long durationNano;
+ /**
+ * The context attached to the span
+ */
+ private final DDSpanContext context;
+
+ private final static Logger logger = LoggerFactory.getLogger(DDSpan.class);
+
+ /**
+ * A simple constructor.
+ * Currently, users have
+ *
+ * @param timestampMicro if set, use this time instead of the auto-generated time
+ * @param context the context
+ */
+ protected DDSpan(
+ long timestampMicro,
+ DDSpanContext context) {
+
+ this.context = context;
+
+ // record the start time in nano (current milli + nano delta)
+ if (timestampMicro == 0L) {
+ this.startTimeMicro = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+ } else {
+ this.startTimeMicro = timestampMicro;
+ }
+ this.startTimeNano = System.nanoTime();
+
+ // track each span of the trace
+ this.context.getTrace().add(this);
+
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#finish()
+ */
+ public void finish() {
+ this.durationNano = System.nanoTime() - startTimeNano;
+ afterFinish();
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#finish(long)
+ */
+ public void finish(long stoptimeMicros) {
+ this.durationNano = TimeUnit.MICROSECONDS.toNanos(stoptimeMicros - this.startTimeMicro);
+ afterFinish();
+ }
+
+ /**
+ * Close the span. If the current span is the parent, check if each child has also been closed
+ * If not, warned it
+ */
+ protected void afterFinish() {
+ logger.debug("{} - Closing the span.", this);
+
+ // warn if one of the parent's children is not finished
+ if (this.isRootSpan()) {
+ logger.debug("{} - The current span is marked as a root span", this);
+ List spans = this.context.getTrace();
+ logger.debug("{} - Checking {} children attached to the current span", this, spans.size());
+
+ for (Span span : spans) {
+ if (((DDSpan) span).getDurationNano() == 0L) {
+ logger.warn("{} - The parent span is marked as finished but this span isn't. You have to close each children.", this);
+ }
+ }
+ this.context.getTracer().write(this.context.getTrace());
+ logger.debug("{} - Sending the trace to the writer", this);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#close()
+ */
+ public void close() {
+ this.finish();
+ }
+
+ /**
+ * Check if the span is the root parent. It means that the traceId is the same as the spanId
+ *
+ * @return true if root, false otherwise
+ */
+ private boolean isRootSpan() {
+ return context.getTraceId() == context.getSpanId();
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#setTag(java.lang.String, java.lang.String)
+ */
+ public Span setTag(String tag, String value) {
+ this.context().setTag(tag, (Object) value);
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#setTag(java.lang.String, boolean)
+ */
+ public Span setTag(String tag, boolean value) {
+ this.context().setTag(tag, (Object) value);
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#setTag(java.lang.String, java.lang.Number)
+ */
+ public Span setTag(String tag, Number value) {
+ this.context().setTag(tag, (Object) value);
+ return this;
+ }
+
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#context()
+ */
+ public DDSpanContext context() {
+ return this.context;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#setBaggageItem(java.lang.String, java.lang.String)
+ */
+ public Span setBaggageItem(String key, String value) {
+ this.context.setBaggageItem(key, value);
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#getBaggageItem(java.lang.String)
+ */
+ public String getBaggageItem(String key) {
+ return this.context.getBaggageItem(key);
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#setOperationName(java.lang.String)
+ */
+ public Span setOperationName(String operationName) {
+ this.context().setOperationName(operationName);
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#log(java.lang.String, java.lang.Object)
+ */
+ public Span log(Map map) {
+ logger.debug("`log` method is not implemented. Doing nothing");
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#log(java.lang.String, java.lang.Object)
+ */
+ public Span log(long l, Map map) {
+ logger.debug("`log` method is not implemented. Doing nothing");
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#log(java.lang.String, java.lang.Object)
+ */
+ public Span log(String s) {
+ logger.debug("`log` method is not implemented. Doing nothing");
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#log(java.lang.String, java.lang.Object)
+ */
+ public Span log(long l, String s) {
+ logger.debug("`log` method is not implemented. Doing nothing");
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#log(java.lang.String, java.lang.Object)
+ */
+ public Span log(String s, Object o) {
+ logger.debug("`log` method is not implemented. Doing nothing");
+ return this;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.Span#log(long, java.lang.String, java.lang.Object)
+ */
+ public Span log(long l, String s, Object o) {
+ logger.debug("`log` method is not implemented. Doing nothing");
+ return this;
+ }
+
+
+ //Getters and JSON serialisation instructions
+
+ /**
+ * Meta merges baggage and tags (stringified values)
+ *
+ * @return merged context baggage and tags
+ */
+ @JsonGetter
+ public Map getMeta() {
+ Map meta = new HashMap();
+ for (Entry entry : context().getBaggageItems().entrySet()) {
+ meta.put(entry.getKey(), entry.getValue());
+ }
+ for (Entry entry : getTags().entrySet()) {
+ meta.put(entry.getKey(), String.valueOf(entry.getValue()));
+ }
+ return meta;
+ }
+
+ @JsonGetter("start")
+ public long getStartTime() {
+ return startTimeMicro * 1000L;
+ }
+
+ @JsonGetter("duration")
+ public long getDurationNano() {
+ return durationNano;
+ }
+
+ @JsonGetter("service")
+ public String getServiceName() {
+ return context.getServiceName();
+ }
+
+ @JsonGetter("trace_id")
+ public long getTraceId() {
+ return context.getTraceId();
+ }
+
+ @JsonGetter("span_id")
+ public long getSpanId() {
+ return context.getSpanId();
+ }
+
+ @JsonGetter("parent_id")
+ public long getParentId() {
+ return context.getParentId();
+ }
+
+ @JsonGetter("resource")
+ public String getResourceName() {
+ return context.getResourceName() == null ? context.getOperationName() : context.getResourceName();
+ }
+
+ @JsonGetter("name")
+ public String getOperationName() {
+ return this.context().getOperationName();
+ }
+
+ @JsonIgnore
+ public Map getTags() {
+ return this.context().getTags();
+ }
+ @JsonGetter
+ public String getType() {
+ return context.getSpanType();
+ }
+
+ @JsonGetter
+ public int getError() {
+ return context.getErrorFlag() ? 1 : 0;
+ }
+
+ @Override
+ public String toString() {
+ return context.toString();
+ }
+
+
+ public Span setServiceName(String serviceName) {
+ this.context().setServiceName(serviceName);
+ return this;
+ }
+
+ public Span setResourceName(String resourceName) {
+ this.context().setResourceName(resourceName);
+ return this;
+ }
+
+ public Span setType(String type) {
+ this.context().setType(type);
+ return this;
+ }
+}
diff --git a/src/main/java/com/datadoghq/trace/impl/DDSpanContext.java b/src/main/java/com/datadoghq/trace/impl/DDSpanContext.java
new file mode 100644
index 00000000000..de2249e3bb5
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/impl/DDSpanContext.java
@@ -0,0 +1,206 @@
+package com.datadoghq.trace.impl;
+
+
+import java.util.*;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import io.opentracing.Span;
+
+/**
+ * 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 Datadog fields that are needed to identify or contextualize
+ * the associated Span instance
+ */
+public class DDSpanContext implements io.opentracing.SpanContext {
+
+ // Opentracing attributes
+ private final long traceId;
+ private final long spanId;
+ private final long parentId;
+ private Map baggageItems;
+
+ // DD attributes
+ /**
+ * The service name is required, otherwise the span are dropped by the agent
+ */
+ private String serviceName;
+ /**
+ * The resource associated to the service (server_web, database, etc.)
+ */
+ private String resourceName;
+ /**
+ * True indicates that the span reports an error
+ */
+ private final boolean errorFlag;
+ /**
+ * The type of the span. If null, the Datadog Agent will report as a custom
+ */
+ private String spanType;
+ /**
+ * The collection of all span related to this one
+ */
+ private final List trace;
+ /**
+ * Each span have an operation name describing the current span
+ */
+ private String operationName;
+ /**
+ * Tags are associated to the current span, they will not propagate to the children span
+ */
+ private Map tags;
+ // Others attributes
+ /**
+ * For technical reasons, the ref to the original tracer
+ */
+ private final DDTracer tracer;
+
+ public DDSpanContext(
+ long traceId,
+ long spanId,
+ long parentId,
+ String serviceName,
+ String operationName,
+ String resourceName,
+ Map baggageItems,
+ boolean errorFlag,
+ String spanType,
+ Map tags,
+ List trace,
+ DDTracer tracer) {
+
+ this.traceId = traceId;
+ this.spanId = spanId;
+ this.parentId = parentId;
+
+ if (baggageItems == null) {
+ this.baggageItems = Collections.emptyMap();
+ } else {
+ this.baggageItems = baggageItems;
+ }
+
+ this.serviceName = serviceName;
+ this.operationName = operationName;
+ this.resourceName = resourceName;
+ this.errorFlag = errorFlag;
+ this.spanType = spanType;
+
+ this.tags = tags;
+
+ if (trace == null) {
+ this.trace = new ArrayList();
+ } else {
+ this.trace = trace;
+ }
+
+ this.tracer = tracer;
+ }
+
+ public long getTraceId() {
+ return this.traceId;
+ }
+
+ public long getParentId() {
+ return this.parentId;
+ }
+
+ public long getSpanId() {
+ return this.spanId;
+ }
+
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+
+ public boolean getErrorFlag() {
+ return errorFlag;
+ }
+
+
+ public String getSpanType() {
+ return spanType;
+ }
+
+ public void setBaggageItem(String key, String value) {
+ if (this.baggageItems.isEmpty()) {
+ this.baggageItems = new HashMap();
+ }
+ this.baggageItems.put(key, value);
+ }
+
+ public String getBaggageItem(String key) {
+ return this.baggageItems.get(key);
+ }
+
+ public Map getBaggageItems() {
+ return baggageItems;
+ }
+
+ /* (non-Javadoc)
+ * @see io.opentracing.SpanContext#baggageItems()
+ */
+ public Iterable> baggageItems() {
+ return this.baggageItems.entrySet();
+ }
+
+ @JsonIgnore
+ public List getTrace() {
+ return this.trace;
+ }
+
+ @JsonIgnore
+ public DDTracer getTracer() {
+ return this.tracer;
+ }
+
+ /**
+ * Add a tag to the span. Tags are not propagated to the children
+ *
+ * @param tag the tag-name
+ * @param value the value of the value
+ * @return the builder instance
+ */
+ public void setTag(String tag, Object value) {
+ if (this.tags.isEmpty()) {
+ this.tags = new HashMap();
+ }
+ this.tags.put(tag, value);
+ }
+
+ @Override
+ public String toString() {
+ return "Span [traceId=" + traceId
+ + ", spanId=" + spanId
+ + ", parentId=" + parentId + "]";
+ }
+
+ public void setOperationName(String operationName) {
+ this.operationName = operationName;
+ }
+
+ public String getOperationName() {
+ return operationName;
+ }
+
+ public Map getTags() {
+ return tags;
+ }
+
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ public void setResourceName(String resourceName) {
+ this.resourceName = resourceName;
+ }
+
+ public void setType(String type) {
+ this.spanType = type;
+ }
+}
diff --git a/src/main/java/com/datadoghq/trace/impl/DDSpanSerializer.java b/src/main/java/com/datadoghq/trace/impl/DDSpanSerializer.java
new file mode 100644
index 00000000000..c37ceb62be1
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/impl/DDSpanSerializer.java
@@ -0,0 +1,35 @@
+package com.datadoghq.trace.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.opentracing.Span;
+
+/**
+ * Main DDSpanSerializer: convert spans and traces to proper JSON
+ */
+public class DDSpanSerializer {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ /* (non-Javadoc)
+ * @see com.datadoghq.trace.DDSpanSerializer#serialize(io.opentracing.Span)
+ */
+ public String serialize(Span span) throws JsonProcessingException {
+ return objectMapper.writeValueAsString(span);
+ }
+
+ /* (non-Javadoc)
+ * @see com.datadoghq.trace.DDSpanSerializer#serialize(java.lang.Object)
+ */
+ public String serialize(Object spans) throws JsonProcessingException {
+ return objectMapper.writeValueAsString(spans);
+ }
+
+ /* (non-Javadoc)
+ * @see com.datadoghq.trace.DDSpanSerializer#deserialize(java.lang.String)
+ */
+ public io.opentracing.Span deserialize(String str) throws Exception {
+ throw new UnsupportedOperationException("Deserialisation of spans is not implemented yet");
+ }
+
+}
diff --git a/src/main/java/com/datadoghq/trace/impl/DDTracer.java b/src/main/java/com/datadoghq/trace/impl/DDTracer.java
new file mode 100644
index 00000000000..187b9775633
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/impl/DDTracer.java
@@ -0,0 +1,243 @@
+package com.datadoghq.trace.impl;
+
+import com.datadoghq.trace.Sampler;
+import com.datadoghq.trace.Writer;
+import com.datadoghq.trace.writer.impl.LoggingWritter;
+import io.opentracing.Span;
+import io.opentracing.SpanContext;
+import io.opentracing.propagation.Format;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+
+/**
+ * DDTracer makes it easy to send traces and span to DD using the OpenTracing instrumentation.
+ */
+public class DDTracer implements io.opentracing.Tracer {
+
+ /**
+ * Writer is an charge of reporting traces and spans to the desired endpoint
+ */
+ private Writer writer;
+ /**
+ * Sampler defines the sampling policy in order to reduce the number of traces for instance
+ */
+ private final Sampler sampler;
+
+
+ private final static Logger logger = LoggerFactory.getLogger(DDTracer.class);
+
+ /**
+ * Default constructor, trace/spans are logged, no trace/span dropped
+ */
+ public DDTracer() {
+ this(new LoggingWritter(), new AllSampler());
+ }
+
+ public DDTracer(Writer writer, Sampler sampler) {
+ this.writer = writer;
+ this.sampler = sampler;
+ }
+
+ public DDSpanBuilder buildSpan(String operationName) {
+ return new DDSpanBuilder(operationName);
+ }
+
+ public void inject(SpanContext spanContext, Format format, C c) {
+ //FIXME Implement it ASAP
+ logger.warn("Method `inject` not implemented yet");
+ }
+
+ public SpanContext extract(Format format, C c) {
+ //FIXME Implement it ASAP
+ logger.warn("Method `inject` not implemented yet");
+ return null;
+ }
+
+
+ /**
+ * We use the sampler to know if the trace has to be reported/written.
+ * The sampler is called on the first span (root span) of the trace.
+ * If the trace is marked as a sample, we report it.
+ *
+ * @param trace a list of the spans related to the same trace
+ */
+ public void write(List trace) {
+ if (trace.isEmpty()) {
+ return;
+ }
+ if (this.sampler.sample((DDSpan) trace.get(0))) {
+ this.writer.write(trace);
+ }
+ }
+
+ /**
+ * Spans are built using this builder
+ */
+ public class DDSpanBuilder implements SpanBuilder {
+
+ /**
+ * Each span must have an operationName according to the opentracing specification
+ */
+ private String operationName;
+
+ // Builder attributes
+ private Map tags = Collections.emptyMap();
+ private long timestamp;
+ private SpanContext parent;
+ private String serviceName;
+ private String resourceName;
+ private boolean errorFlag;
+ private String spanType;
+
+ /**
+ * This method actually build the span according to the builder settings
+ * DD-Agent requires a serviceName. If it has not been provided, the method will throw a RuntimeException
+ *
+ * @return An fresh span
+ */
+ public DDSpan start() {
+
+ // build the context
+ DDSpanContext context = buildSpanContext();
+ DDSpan span = new DDSpan(this.timestamp, context);
+
+ logger.debug("{} - Starting a new span.", span);
+
+ return span;
+ }
+
+
+ public DDTracer.DDSpanBuilder withTag(String tag, Number number) {
+ return withTag(tag, (Object) number);
+ }
+
+ public DDTracer.DDSpanBuilder withTag(String tag, String string) {
+ return withTag(tag, (Object) string);
+ }
+
+ public DDTracer.DDSpanBuilder withTag(String tag, boolean bool) {
+ return withTag(tag, (Object) bool);
+ }
+
+ public DDSpanBuilder(String operationName) {
+ this.operationName = operationName;
+ }
+
+
+ public DDTracer.DDSpanBuilder withStartTimestamp(long timestampMillis) {
+ this.timestamp = timestampMillis;
+ return this;
+ }
+
+ public DDTracer.DDSpanBuilder withServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ return this;
+ }
+
+ public DDTracer.DDSpanBuilder withResourceName(String resourceName) {
+ this.resourceName = resourceName;
+ return this;
+ }
+
+ public DDTracer.DDSpanBuilder withErrorFlag() {
+ this.errorFlag = true;
+ return this;
+ }
+
+ public DDTracer.DDSpanBuilder withSpanType(String spanType) {
+ this.spanType = spanType;
+ return this;
+ }
+
+ public Iterable> baggageItems() {
+ if (parent == null) {
+ return Collections.emptyList();
+ }
+ return parent.baggageItems();
+ }
+
+ public DDTracer.DDSpanBuilder asChildOf(Span span) {
+ return asChildOf(span.context());
+ }
+
+ public DDTracer.DDSpanBuilder asChildOf(SpanContext spanContext) {
+ this.parent = spanContext;
+ return this;
+ }
+
+ public DDTracer.DDSpanBuilder addReference(String referenceType, SpanContext spanContext) {
+ logger.debug("`addReference` method is not implemented. Doing nothing");
+ return this;
+ }
+
+ // Private methods
+ private DDTracer.DDSpanBuilder withTag(String tag, Object value) {
+ if (this.tags.isEmpty()){
+ this.tags = new HashMap();
+ }
+ this.tags.put(tag, value);
+ return this;
+ }
+
+ private long generateNewId() {
+ return System.nanoTime();
+ }
+
+ /**
+ * Build the SpanContext, if the actual span has a parent, the following attributes must be propagated:
+ * - ServiceName
+ * - Baggage
+ * - Trace (a list of all spans related)
+ * - SpanType
+ *
+ * @return the context
+ */
+ private DDSpanContext buildSpanContext() {
+ long generatedId = generateNewId();
+ DDSpanContext context;
+ DDSpanContext p = this.parent != null ? (DDSpanContext) this.parent : null;
+
+ String spanType = this.spanType;
+ if (spanType == null && this.parent != null) {
+ spanType = p.getSpanType();
+ }
+
+ String serviceName = this.serviceName;
+ if (serviceName == null && this.parent != null) {
+ serviceName = p.getServiceName();
+ }
+
+ //this.operationName, this.tags,
+
+ // some attributes are inherited from the parent
+ context = new DDSpanContext(
+ this.parent == null ? generatedId : p.getTraceId(),
+ generatedId,
+ this.parent == null ? 0L : p.getSpanId(),
+ serviceName,
+ this.operationName,
+ this.resourceName,
+ this.parent == null ? Collections.emptyMap() : p.getBaggageItems(),
+ errorFlag,
+ spanType,
+ this.tags,
+ this.parent == null ? null : p.getTrace(),
+ DDTracer.this
+ );
+
+ logger.debug("Building a new span context. {}", context);
+ return context;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return "DDTracer{" +
+ "writer=" + writer +
+ ", sampler=" + sampler +
+ '}';
+ }
+}
diff --git a/src/main/java/com/datadoghq/trace/impl/RateSampler.java b/src/main/java/com/datadoghq/trace/impl/RateSampler.java
new file mode 100644
index 00000000000..afa5b49c66d
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/impl/RateSampler.java
@@ -0,0 +1,55 @@
+package com.datadoghq.trace.impl;
+
+
+import com.datadoghq.trace.Sampler;
+import io.opentracing.Span;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * This sampler sample the traces at a predefined rate.
+ *
+ * Keep (100 * `sample_rate`)% of the traces.
+ * It samples randomly, its main purpose is to reduce the instrumentation footprint.
+ */
+public class RateSampler implements Sampler {
+
+
+ private final static Logger logger = LoggerFactory.getLogger(RateSampler.class);
+ /**
+ * The sample rate used
+ */
+ private final double sampleRate;
+
+ /**
+ * Build an instance of the sampler. The Sample rate is fixed for each instance.
+ *
+ * @param sampleRate a number [0,1] representing the rate ratio.
+ */
+ public RateSampler(double sampleRate) {
+
+ if (sampleRate <= 0) {
+ sampleRate = 1;
+ logger.error("SampleRate is negative or null, disabling the sampler");
+ } else if (sampleRate > 1) {
+ sampleRate = 1;
+ }
+
+ this.sampleRate = sampleRate;
+ logger.debug("Initializing the RateSampler, sampleRate: {} %", this.sampleRate * 100);
+
+ }
+
+ @Override
+ public boolean sample(Span span) {
+ boolean sample = Math.random() <= this.sampleRate;
+ logger.debug("{} - Span is sampled: {}", span, sample);
+ return sample;
+ }
+
+ public double getSampleRate() {
+ return this.sampleRate;
+ }
+
+}
diff --git a/src/main/java/com/datadoghq/trace/writer/impl/DDAgentWriter.java b/src/main/java/com/datadoghq/trace/writer/impl/DDAgentWriter.java
new file mode 100644
index 00000000000..817dd0b2ebd
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/writer/impl/DDAgentWriter.java
@@ -0,0 +1,138 @@
+package com.datadoghq.trace.writer.impl;
+
+import com.datadoghq.trace.Writer;
+import io.opentracing.Span;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+
+/**
+ * This writer write provided traces to the a DD agent which is most of time located on the same host.
+ *
+ * It handles writes asynchronuously so the calling threads are automatically released. However, if too much spans are collected
+ * the writers can reach a state where it is forced to drop incoming spans.
+ */
+public class DDAgentWriter implements Writer {
+
+ private static final Logger logger = LoggerFactory.getLogger(DDAgentWriter.class.getName());
+
+ /**
+ * Default location of the DD agent
+ */
+ private static final String DEFAULT_HOSTNAME = "localhost";
+ private static final int DEFAULT_PORT = 8126;
+
+ /**
+ * Maximum number of spans kept in memory
+ */
+ private static final int DEFAULT_MAX_SPANS = 1000;
+
+ /**
+ * Maximum number of traces sent to the DD agent API at once
+ */
+ private static final int DEFAULT_BATCH_SIZE = 10;
+
+ /**
+ * Used to ensure that we don't keep too many spans (while the blocking queue collect traces...)
+ */
+ private final Semaphore tokens;
+
+ /**
+ * In memory collection of traces waiting for departure
+ */
+ private final BlockingQueue> traces;
+
+ /**
+ * Async worker that posts the spans to the DD agent
+ */
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ /**
+ * The DD agent api
+ */
+ private final DDApi api;
+
+ public DDAgentWriter() {
+ this(new DDApi(DEFAULT_HOSTNAME, DEFAULT_PORT));
+ }
+
+ public DDAgentWriter(DDApi api) {
+ super();
+ this.api = api;
+
+ tokens = new Semaphore(DEFAULT_MAX_SPANS);
+ traces = new ArrayBlockingQueue>(DEFAULT_MAX_SPANS);
+
+ executor.submit(new SpansSendingTask());
+
+ }
+
+ /* (non-Javadoc)
+ * @see com.datadoghq.trace.Writer#write(java.util.List)
+ */
+ public void write(List trace) {
+ //Try to add a new span in the queue
+ boolean proceed = tokens.tryAcquire(trace.size());
+
+ if (proceed) {
+ traces.add(trace);
+ } else {
+ logger.warn("Cannot add a trace of {} as the async queue is full. Queue max size: {}", trace.size(), DEFAULT_MAX_SPANS);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.datadoghq.trace.Writer#close()
+ */
+ public void close() {
+ executor.shutdownNow();
+ try {
+ executor.awaitTermination(500, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ logger.info("Writer properly closed and async writer interrupted.");
+ }
+ }
+
+ /**
+ * Infinite tasks blocking until some spans come in the blocking queue.
+ */
+ protected class SpansSendingTask implements Runnable {
+
+ public void run() {
+ while (true) {
+ try {
+ List> payload = new ArrayList>();
+
+ //WAIT until a new span comes
+ List l = DDAgentWriter.this.traces.take();
+ payload.add(l);
+
+ //Drain all spans up to a certain batch suze
+ traces.drainTo(payload, DEFAULT_BATCH_SIZE);
+
+ //SEND the payload to the agent
+ logger.debug("Async writer about to write {} traces.", payload.size());
+ api.sendTraces(payload);
+
+ //Compute the number of spans sent
+ int spansCount = 0;
+ for (List trace : payload) {
+ spansCount += trace.size();
+ }
+ logger.debug("Async writer just sent {} spans through {} traces", spansCount, payload.size());
+
+ //Release the tokens
+ tokens.release(spansCount);
+ } catch (InterruptedException e) {
+ logger.info("Async writer interrupted.");
+
+ //The thread was interrupted, we break the LOOP
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/datadoghq/trace/writer/impl/DDApi.java b/src/main/java/com/datadoghq/trace/writer/impl/DDApi.java
new file mode 100644
index 00000000000..746449b63fe
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/writer/impl/DDApi.java
@@ -0,0 +1,108 @@
+package com.datadoghq.trace.writer.impl;
+
+import com.datadoghq.trace.impl.DDSpanSerializer;
+import io.opentracing.Span;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.List;
+
+/**
+ * The API pointing to a DD agent
+ */
+public class DDApi {
+
+ private static final Logger logger = LoggerFactory.getLogger(DDApi.class.getName());
+
+ private static final String TRACES_ENDPOINT = "/v0.3/traces";
+ private static final String SERVICES_ENDPOINT = "/v0.3/services";
+
+ private final String host;
+ private final int port;
+ private final String tracesEndpoint;
+ private final String servicesEndpoint;
+
+ /**
+ * The spans serializer: can be replaced. By default, it serialize in JSON.
+ */
+ private final DDSpanSerializer spanSerializer;
+
+ public DDApi(String host, int port) {
+ this(host, port, new DDSpanSerializer());
+ }
+
+ public DDApi(String host, int port, DDSpanSerializer spanSerializer) {
+ super();
+ this.host = host;
+ this.port = port;
+ this.tracesEndpoint = "http://" + host + ":" + port + TRACES_ENDPOINT;
+ this.servicesEndpoint = "http://" + host + ":" + port + SERVICES_ENDPOINT;
+ this.spanSerializer = spanSerializer;
+ }
+
+ /**
+ * Send traces to the DD agent
+ *
+ * @param traces the traces to be sent
+ * @return the staus code returned
+ */
+ public boolean sendTraces(List> traces) {
+ String payload = null;
+ try {
+ payload = spanSerializer.serialize(traces);
+ } catch (Exception e) {
+ logger.error("Error during serialization of " + traces.size() + " traces.", e);
+ return false;
+ }
+
+ int status = callPUT(tracesEndpoint, payload);
+ if (status == 200) {
+ logger.debug("Succesfully sent {} traces to the DD agent.", traces.size());
+ return true;
+ } else {
+ logger.warn("Error while sending {} traces to the DD agent. Status: {}", traces.size(), status);
+ return false;
+ }
+ }
+
+ /**
+ * PUT to an endpoint the provided JSON content
+ *
+ * @param endpoint
+ * @param content
+ * @return the status code
+ */
+ private int callPUT(String endpoint, String content) {
+ HttpURLConnection httpCon = null;
+ try {
+ URL url = new URL(endpoint);
+ httpCon = (HttpURLConnection) url.openConnection();
+ httpCon.setDoOutput(true);
+ httpCon.setRequestMethod("PUT");
+ httpCon.setRequestProperty("Content-Type", "application/json");
+ } catch (Exception e) {
+ logger.warn("Error thrown before PUT call to the DD agent.", e);
+ return -1;
+ }
+
+ try {
+ OutputStreamWriter out = new OutputStreamWriter(httpCon.getOutputStream());
+ out.write(content);
+ out.close();
+ int responseCode = httpCon.getResponseCode();
+ if (responseCode != 200) {
+ logger.debug("Sent the payload to the DD agent.");
+ } else {
+ logger.warn("Could not send the payload to the DD agent. Status: {} ResponseMessage: {}", httpCon.getResponseCode(), httpCon.getResponseMessage());
+ }
+ return responseCode;
+ } catch (Exception e) {
+ logger.warn("Could not send the payload to the DD agent.", e);
+ return -1;
+ }
+ }
+
+}
diff --git a/src/main/java/com/datadoghq/trace/writer/impl/LoggingWritter.java b/src/main/java/com/datadoghq/trace/writer/impl/LoggingWritter.java
new file mode 100644
index 00000000000..e637df00e30
--- /dev/null
+++ b/src/main/java/com/datadoghq/trace/writer/impl/LoggingWritter.java
@@ -0,0 +1,25 @@
+package com.datadoghq.trace.writer.impl;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datadoghq.trace.Writer;
+
+import io.opentracing.Span;
+
+public class LoggingWritter implements Writer{
+
+ private static final Logger logger = LoggerFactory.getLogger(LoggingWritter.class.getName());
+
+ @Override
+ public void write(List trace) {
+ logger.info("write(trace): {}", trace);
+ }
+
+ @Override
+ public void close() {
+ logger.info("close()");
+ }
+}
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 00000000000..8e47d59df8c
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/ExampleWithDDAgentWriter.java b/src/test/java/ExampleWithDDAgentWriter.java
new file mode 100644
index 00000000000..06f45ac7573
--- /dev/null
+++ b/src/test/java/ExampleWithDDAgentWriter.java
@@ -0,0 +1,53 @@
+import com.datadoghq.trace.Sampler;
+import com.datadoghq.trace.Writer;
+import com.datadoghq.trace.impl.AllSampler;
+import com.datadoghq.trace.impl.DDTracer;
+import com.datadoghq.trace.writer.impl.DDAgentWriter;
+import io.opentracing.Span;
+
+public class ExampleWithDDAgentWriter {
+
+ public static void main(String[] args) throws Exception {
+
+ // Instantiate the DDWriter
+ // By default, traces are written to localhost:8126 (the ddagent)
+ Writer writer = new DDAgentWriter();
+
+ // Instantiate the proper Sampler
+ // - RateSampler if you want to keep `ratio` traces
+ // - AllSampler to keep all traces
+ Sampler sampler = new AllSampler();
+
+
+ // Create the tracer
+ DDTracer tracer = new DDTracer(writer, sampler);
+
+
+ Span parent = tracer
+ .buildSpan("hello-world")
+ .withServiceName("service-name")
+ .withSpanType("web")
+ .start();
+
+ Thread.sleep(100);
+
+ parent.setBaggageItem("a-baggage", "value");
+
+ Span child = tracer
+ .buildSpan("hello-world")
+ .asChildOf(parent)
+ .withResourceName("resource-name")
+ .start();
+
+ Thread.sleep(100);
+
+ child.finish();
+
+ Thread.sleep(100);
+
+ parent.finish();
+
+ writer.close();
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ExampleWithLoggingWriter.java b/src/test/java/ExampleWithLoggingWriter.java
new file mode 100644
index 00000000000..f919f9f6782
--- /dev/null
+++ b/src/test/java/ExampleWithLoggingWriter.java
@@ -0,0 +1,41 @@
+import java.util.ArrayList;
+import java.util.List;
+
+import com.datadoghq.trace.Writer;
+import com.datadoghq.trace.impl.DDTracer;
+import com.datadoghq.trace.writer.impl.DDAgentWriter;
+
+import io.opentracing.Span;
+
+public class ExampleWithLoggingWriter {
+
+ public static void main(String[] args) throws Exception {
+
+ DDTracer tracer = new DDTracer();
+
+ Span parent = tracer
+ .buildSpan("hello-world")
+ .withServiceName("service-name")
+ .withSpanType("web")
+ .start();
+
+ parent.setBaggageItem("a-baggage", "value");
+
+ Thread.sleep(100);
+
+ Span child = tracer
+ .buildSpan("hello-world")
+ .asChildOf(parent)
+ .withResourceName("resource-name")
+ .start();
+
+ Thread.sleep(100);
+
+ child.finish();
+
+ Thread.sleep(100);
+
+ parent.finish();
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/datadoghq/trace/impl/DDSpanBuilderTest.java b/src/test/java/com/datadoghq/trace/impl/DDSpanBuilderTest.java
new file mode 100644
index 00000000000..ada1b333aa7
--- /dev/null
+++ b/src/test/java/com/datadoghq/trace/impl/DDSpanBuilderTest.java
@@ -0,0 +1,233 @@
+package com.datadoghq.trace.impl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+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";
+ DDSpan span = tracer.buildSpan(expectedName).withServiceName("foo").start();
+ assertThat(span.getOperationName()).isEqualTo(expectedName);
+ }
+
+ @Test
+ public void shouldBuildMoreComplexSpan() {
+
+ final String expectedName = "fakeName";
+ final Map tags = new HashMap() {
+ {
+ 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"))
+ .start();
+
+ assertThat(span.getOperationName()).isEqualTo(expectedName);
+ assertThat(span.getTags()).containsAllEntriesOf(tags);
+
+ // with no tag provided
+
+ span = tracer
+ .buildSpan(expectedName)
+ .withServiceName("foo")
+ .start();
+
+ assertThat(span.getTags()).isNotNull();
+ assertThat(span.getTags()).isEmpty();
+
+ // 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();
+
+ DDSpanContext actualContext = span.context();
+
+ assertThat(actualContext.getResourceName()).isEqualTo(expectedResource);
+ assertThat(actualContext.getErrorFlag()).isTrue();
+ assertThat(actualContext.getServiceName()).isEqualTo(expectedService);
+ assertThat(actualContext.getSpanType()).isEqualTo(expectedType);
+
+ }
+
+ @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
+ long tick = System.currentTimeMillis() * 1000 * 1000L;
+ span = tracer
+ .buildSpan(expectedName)
+ .withServiceName("foo")
+ .start();
+
+ // between now and now + 100ms
+ assertThat(span.getStartTime()).isBetween(tick, tick + 100 * 1000L);
+
+ }
+
+
+ @Test
+ public void shouldLinkToParentSpan() {
+
+ final long spanId = 1L;
+ final long expectedParentId = spanId;
+
+ DDSpanContext mockedContext = mock(DDSpanContext.class);
+ DDSpan mockedSpan = mock(DDSpan.class);
+
+ when(mockedSpan.context()).thenReturn(mockedContext);
+ when(mockedContext.getSpanId()).thenReturn(spanId);
+ when(mockedContext.getServiceName()).thenReturn("foo");
+
+ final String expectedName = "fakeName";
+
+ DDSpan span = tracer
+ .buildSpan(expectedName)
+ .withServiceName("foo")
+ .asChildOf(mockedSpan)
+ .start();
+
+ 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";
+
+ 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();
+
+ 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)
+ .start();
+
+ 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 {
+
+ ArrayList spans = new ArrayList();
+ final int nbSamples = 10;
+
+ // root (aka spans[0]) is the parent
+ // others are just for fun
+
+ DDSpan root = tracer.buildSpan("fake_O").withServiceName("foo").start();
+ spans.add(root);
+
+
+ Thread.sleep(200);
+ 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);
+
+ assertThat(root.context().getTrace()).hasSize(nbSamples + 1);
+ assertThat(root.context().getTrace()).containsAll(spans);
+ assertThat(spans.get((int) (Math.random() * nbSamples)).context().getTrace()).containsAll(spans);
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/datadoghq/trace/impl/DDSpanSerializerTest.java b/src/test/java/com/datadoghq/trace/impl/DDSpanSerializerTest.java
new file mode 100644
index 00000000000..eb35b3770a0
--- /dev/null
+++ b/src/test/java/com/datadoghq/trace/impl/DDSpanSerializerTest.java
@@ -0,0 +1,61 @@
+package com.datadoghq.trace.impl;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DDSpanSerializerTest {
+
+
+ DDSpanSerializer serializer;
+ DDSpan span;
+
+ @Before
+ public void setUp() throws Exception {
+
+ Map baggage = new HashMap();
+ baggage.put("a-baggage", "value");
+ Map tags = new HashMap();
+ baggage.put("k1", "v1");
+
+
+ DDSpanContext context = new DDSpanContext(
+ 1L,
+ 2L,
+ 0L,
+ "service",
+ "operation",
+ "resource",
+ baggage,
+ false,
+ "type",
+ tags,
+ null,
+ null);
+
+ span = new DDSpan(
+ 100L,
+ context);
+
+ span.finish(133L);
+ serializer = new DDSpanSerializer();
+ }
+
+ @Test
+ public void test() throws Exception {
+
+
+ String expected = "{\"meta\":{\"a-baggage\":\"value\",\"k1\":\"v1\"},\"service\":\"service\",\"error\":0,\"type\":\"type\",\"name\":\"operation\",\"duration\":33000,\"resource\":\"resource\",\"start\":100000,\"span_id\":2,\"parent_id\":0,\"trace_id\":1}";
+ // FIXME At the moment, just compare the string sizes
+ try {
+ assertThat(serializer.serialize(span).length()).isEqualTo(expected.length());
+ } catch (AssertionError e) {
+ assertThat(serializer.serialize(span)).isEqualTo(expected);
+ }
+ }
+
+}
diff --git a/src/test/java/com/datadoghq/trace/impl/DDSpanTest.java b/src/test/java/com/datadoghq/trace/impl/DDSpanTest.java
new file mode 100644
index 00000000000..b97a8e5cf8a
--- /dev/null
+++ b/src/test/java/com/datadoghq/trace/impl/DDSpanTest.java
@@ -0,0 +1,74 @@
+package com.datadoghq.trace.impl;
+
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+public class DDSpanTest {
+
+
+ @Test
+ public void testGetterSetter() {
+
+ DDSpanContext context = new DDSpanContext(
+ 1L,
+ 1L,
+ 0L,
+ "fakeService",
+ "fakeOperation",
+ "fakeResource",
+ Collections.emptyMap(),
+ false,
+ "fakeType",
+ null,
+ null,
+ null);
+
+
+ String expected;
+ DDSpan span = new DDSpan(1L, context);
+
+ expected = "service";
+ span.setServiceName(expected);
+ assertThat(span.getServiceName()).isEqualTo(expected);
+
+ expected = "operation";
+ span.setOperationName(expected);
+ assertThat(span.getOperationName()).isEqualTo(expected);
+
+ expected = "resource";
+ span.setResourceName(expected);
+ assertThat(span.getResourceName()).isEqualTo(expected);
+
+ expected = "type";
+ span.setType(expected);
+ assertThat(span.getType()).isEqualTo(expected);
+
+ }
+
+
+ @Test
+ public void shouldResourceNameEqualsOperationNameIfNull() {
+
+ final String expectedName = "operationName";
+
+ DDSpan span = new DDTracer().buildSpan(expectedName).withServiceName("foo").start();
+ // ResourceName = expectedName
+ assertThat(span.getResourceName()).isEqualTo(expectedName);
+
+ // ResourceName = expectedResourceName
+ final String expectedResourceName = "fake";
+ span = new DDTracer()
+ .buildSpan(expectedName)
+ .withResourceName(expectedResourceName)
+ .withServiceName("foo").start();
+
+ assertThat(span.getResourceName()).isEqualTo(expectedResourceName);
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/datadoghq/trace/impl/DDTracerTest.java b/src/test/java/com/datadoghq/trace/impl/DDTracerTest.java
new file mode 100644
index 00000000000..295120238fc
--- /dev/null
+++ b/src/test/java/com/datadoghq/trace/impl/DDTracerTest.java
@@ -0,0 +1,49 @@
+package com.datadoghq.trace.impl;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.datadoghq.trace.Writer;
+
+import io.opentracing.Span;
+
+
+public class DDTracerTest {
+
+
+ @Test
+ public void write() throws Exception {
+
+ Writer writer = mock(Writer.class);
+ RateSampler sampler = mock(RateSampler.class);
+ DDSpan span = mock(DDSpan.class);
+
+ // Rate 0.5
+ when(sampler.sample(any(DDSpan.class)))
+ .thenReturn(true)
+ .thenReturn(false);
+
+ List spans = new ArrayList();
+ spans.add(span);
+ spans.add(span);
+ spans.add(span);
+
+ DDTracer tracer = new DDTracer(writer, sampler);
+
+ tracer.write(spans);
+ tracer.write(spans);
+
+ verify(sampler, times(2)).sample(span);
+ verify(writer, times(1)).write(spans);
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/datadoghq/trace/impl/RateSamplerTest.java b/src/test/java/com/datadoghq/trace/impl/RateSamplerTest.java
new file mode 100644
index 00000000000..811f363df98
--- /dev/null
+++ b/src/test/java/com/datadoghq/trace/impl/RateSamplerTest.java
@@ -0,0 +1,46 @@
+package com.datadoghq.trace.impl;
+
+import com.datadoghq.trace.Sampler;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class RateSamplerTest {
+
+
+ @Test
+ public void testRateSampler() {
+
+ DDSpan mockSpan = mock(DDSpan.class);
+
+ final double sampleRate = 0.35;
+ final int iterations = 1000;
+ Sampler sampler = new RateSampler(sampleRate);
+
+ int kept = 0;
+
+ for (int i = 0; i < iterations; i++) {
+ if (sampler.sample(mockSpan)) {
+ kept++;
+ }
+ }
+ //FIXME test has to be more predictable
+ //assertThat(((double) kept / iterations)).isBetween(sampleRate - 0.02, sampleRate + 0.02);
+
+ }
+
+ @Test
+ public void testRateBoundaries() {
+
+ RateSampler sampler = new RateSampler(1000);
+ assertThat(sampler.getSampleRate()).isEqualTo(1);
+
+ sampler = new RateSampler(-1000);
+ assertThat(sampler.getSampleRate()).isEqualTo(1);
+
+ sampler = new RateSampler(0.337);
+ assertThat(sampler.getSampleRate()).isEqualTo(0.337);
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/datadoghq/trace/writer/impl/DDAgentWriterTest.java b/src/test/java/com/datadoghq/trace/writer/impl/DDAgentWriterTest.java
new file mode 100644
index 00000000000..e5b82bd3ece
--- /dev/null
+++ b/src/test/java/com/datadoghq/trace/writer/impl/DDAgentWriterTest.java
@@ -0,0 +1,66 @@
+package com.datadoghq.trace.writer.impl;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datadoghq.trace.impl.DDSpan;
+import com.datadoghq.trace.impl.DDTracer;
+
+import io.opentracing.Span;
+
+public class DDAgentWriterTest {
+
+ DDSpan parent = null;
+ DDApi mockedAPI = null;
+ List> traces = new ArrayList>();
+ DDAgentWriter ddAgentWriter = null;
+
+ @Before
+ public void setUp() throws Exception {
+ //Setup
+ DDTracer tracer = new DDTracer();
+
+ parent = tracer.buildSpan("hello-world").withServiceName("service-name").start();
+ parent.setBaggageItem("a-baggage", "value");
+
+ Thread.sleep(100);
+
+ DDSpan child = tracer.buildSpan("hello-world").asChildOf(parent).start();
+ Thread.sleep(100);
+
+ child.finish();
+ Thread.sleep(100);
+ parent.finish();
+
+ //Create DDWriter
+ traces.add(parent.context().getTrace());
+ mockedAPI = mock(DDApi.class);
+ when(mockedAPI.sendTraces(traces)).thenReturn(true);
+ ddAgentWriter = new DDAgentWriter(mockedAPI);
+ }
+
+ @Test
+ public void testWrite() throws Exception{
+ ddAgentWriter.write(parent.context().getTrace());
+ Thread.sleep(500);
+ verify(mockedAPI).sendTraces(traces);
+ }
+
+ @Test
+ public void testClose() throws Exception{
+ ddAgentWriter.close();
+
+ ddAgentWriter.write(parent.context().getTrace());
+ Thread.sleep(500);
+ verifyNoMoreInteractions(mockedAPI);
+ }
+
+}