diff --git a/.gitignore b/.gitignore index b34c34cac6c..b14fe2c1000 100644 --- a/.gitignore +++ b/.gitignore @@ -1,156 +1,34 @@ +# Maven # +######### +target -# Created by https://www.gitignore.io/api/java,maven,eclipse,intellij - -### Eclipse ### - -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" +# Eclipse # +########### *.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# Java annotation processor (APT) -.factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) -.springBeans - -# Code Recommenders -.recommenders/ - -# Scala IDE specific (Scala & Java development for Eclipse) -.cache-main -.scala_dependencies -.worksheet - -### Intellij ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff: -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.xml -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -# Gradle: -.idea/**/gradle.xml -.idea/**/libraries - -# CMake -cmake-build-debug/ - -# Mongo Explorer plugin: -.idea/**/mongoSettings.xml - -## File-based project format: -*.iws - -## Plugin-specific files: - -# IntelliJ -/out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -### Intellij Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -.idea/sonarlint - -### Java ### -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -### Maven ### -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties - -# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) -!/.mvn/wrapper/maven-wrapper.jar - -# End of https://www.gitignore.io/api/java,maven,eclipse,intellij +.settings +.project +.classpath + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db + +# Intellij Idea # +################# +/.idea +*.iml + +# Visual Studio Code # +###################### +.vscode + +# Others # +########## +/logs/* diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000000..a80435dff5d --- /dev/null +++ b/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + com.datadog + raclette-java + 1.0-SNAPSHOT + + + + io.opentracing + opentracing-api + 0.21.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.8.8 + + + + + org.slf4j + slf4j-api + 1.7.25 + + + ch.qos.logback + logback-classic + 1.2.3 + + + net.logstash.logback + logstash-logback-encoder + 4.9 + + + + + junit + junit + 4.12 + test + + + org.assertj + assertj-core + 3.6.2 + test + + + org.mockito + mockito-core + 2.7.22 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + \ No newline at end of file diff --git a/src/main/java/com/datadoghq/trace/Sampler.java b/src/main/java/com/datadoghq/trace/Sampler.java new file mode 100644 index 00000000000..bf1d137b1c1 --- /dev/null +++ b/src/main/java/com/datadoghq/trace/Sampler.java @@ -0,0 +1,19 @@ +package com.datadoghq.trace; + + +import io.opentracing.Span; + +/** + * Main interface to sample a collection of traces. + */ +public interface Sampler { + + /** + * Sample a collection of traces based on the parent span + * + * @param span the parent span with its context + * @return true when the trace/spans has to be reported/written + */ + boolean sample(Span span); + +} diff --git a/src/main/java/com/datadoghq/trace/Writer.java b/src/main/java/com/datadoghq/trace/Writer.java new file mode 100644 index 00000000000..8bcba88b326 --- /dev/null +++ b/src/main/java/com/datadoghq/trace/Writer.java @@ -0,0 +1,23 @@ +package com.datadoghq.trace; + +import java.util.List; + +import io.opentracing.Span; + +/** + * A writer is responsible to send collected spans to some place + */ +public interface Writer { + + /** + * Write a trace represented by the entire list of all the finished spans + * + * @param trace the list of spans to write + */ + void write(List 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); + } + +}