Initial support for JSON rendering in new API
MODIFY
dd-trace/src/main/java/datadog/trace/tracer/SpanImpl.java
dd-trace/src/main/java/datadog/trace/tracer/SpanImpl.java
package datadog.trace.tracer;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import datadog.trace.api.DDTags;
+import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -10,6 +20,13 @@ import lombok.extern.slf4j.Slf4j;
/** Concrete implementation of a span */
@Slf4j
+// Disable autodetection of fields and accessors
+@JsonAutoDetect(
+ fieldVisibility = Visibility.NONE,
+ setterVisibility = Visibility.NONE,
+ getterVisibility = Visibility.NONE,
+ isGetterVisibility = Visibility.NONE,
+ creatorVisibility = Visibility.NONE)
class SpanImpl implements Span {
private final TraceInternal trace;
@@ -68,12 +85,32 @@ class SpanImpl implements Span {
return context;
}
+ @JsonGetter("trace_id")
+ @JsonSerialize(using = UInt64IDStringSerializer.class)
+ public String getTraceId() {
+ return context.getTraceId();
+ }
+
+ @JsonGetter("span_id")
+ @JsonSerialize(using = UInt64IDStringSerializer.class)
+ public String getSpanId() {
+ return context.getSpanId();
+ }
+
+ @JsonGetter("parent_id")
+ @JsonSerialize(using = UInt64IDStringSerializer.class)
+ public String getParentId() {
+ return context.getParentId();
+ }
+
@Override
+ @JsonGetter("start")
public Timestamp getStartTimestamp() {
return startTimestamp;
}
@Override
+ @JsonGetter("duration")
public Long getDuration() {
return duration;
}
@@ -84,6 +121,7 @@ class SpanImpl implements Span {
}
@Override
+ @JsonGetter("service")
public String getService() {
return service;
}
@@ -98,6 +136,7 @@ class SpanImpl implements Span {
}
@Override
+ @JsonGetter("resource")
public String getResource() {
return resource;
}
@@ -112,6 +151,7 @@ class SpanImpl implements Span {
}
@Override
+ @JsonGetter("type")
public String getType() {
return type;
}
@@ -126,6 +166,7 @@ class SpanImpl implements Span {
}
@Override
+ @JsonGetter("name")
public String getName() {
return name;
}
@@ -140,6 +181,8 @@ class SpanImpl implements Span {
}
@Override
+ @JsonGetter("error")
+ @JsonFormat(shape = JsonFormat.Shape.NUMBER)
public boolean isErrored() {
return errored;
}
@@ -169,6 +212,15 @@ class SpanImpl implements Span {
}
}
+ @JsonGetter("meta")
+ synchronized Map<String, String> getMeta() {
+ final Map<String, String> result = new HashMap<>(meta.size());
+ for (final Map.Entry<String, Object> entry : meta.entrySet()) {
+ result.put(entry.getKey(), String.valueOf(entry.getValue()));
+ }
+ return result;
+ }
+
@Override
public synchronized Object getMeta(final String key) {
return meta.get(key);
@@ -201,6 +253,8 @@ class SpanImpl implements Span {
setMeta(key, (Object) value);
}
+ // FIXME: Add metrics support and json rendering for metrics
+
@Override
public synchronized void finish() {
if (isFinished()) {
@@ -263,4 +317,19 @@ class SpanImpl implements Span {
private void reportSetterUsageError(final String fieldName) {
reportUsageError("Attempted to set '%s' when span is already finished: %s", fieldName, this);
}
+
+ /** Helper to serialize string value as 64 bit unsigned integer */
+ private static class UInt64IDStringSerializer extends StdSerializer<String> {
+
+ public UInt64IDStringSerializer() {
+ super(String.class);
+ }
+
+ @Override
+ public void serialize(
+ final String value, final JsonGenerator jsonGenerator, final SerializerProvider provider)
+ throws IOException {
+ jsonGenerator.writeNumber(new BigInteger(value));
+ }
+ }
}

MODIFY
dd-trace/src/main/java/datadog/trace/tracer/Timestamp.java
dd-trace/src/main/java/datadog/trace/tracer/Timestamp.java
@@ -2,6 +2,7 @@ package datadog.trace.tracer;
import static java.lang.Math.max;
+import com.fasterxml.jackson.annotation.JsonValue;
import lombok.EqualsAndHashCode;
/**
@@ -30,9 +31,15 @@ public class Timestamp {
return clock;
}
+ /** @return time since epoch in nanoseconds */
+ @JsonValue
+ public long getTime() {
+ return clock.getStartTimeNano() + startTicksOffset();
+ }
+
/** @return duration in nanoseconds from this time stamp to current time. */
public long getDuration() {
- return getDuration(new Timestamp(clock));
+ return getDuration(clock.createCurrentTimestamp());
}
/**
@@ -50,10 +57,7 @@ public class Timestamp {
clock, finishTimestamp.clock);
// Do our best to try to calculate nano-second time using millisecond clock start time and
// nanosecond offset.
- return max(
- 0,
- (finishTimestamp.clock.getStartTimeNano() + finishTimestamp.startTicksOffset())
- - (clock.getStartTimeNano() + startTicksOffset()));
+ return max(0, finishTimestamp.getTime() - getTime());
}
return max(0, finishTimestamp.nanoTicks - nanoTicks);
}

MODIFY
dd-trace/src/main/java/datadog/trace/tracer/TraceImpl.java
dd-trace/src/main/java/datadog/trace/tracer/TraceImpl.java
package datadog.trace.tracer;
+import com.fasterxml.jackson.annotation.JsonValue;
import datadog.trace.tracer.writer.Writer;
import java.util.ArrayList;
import java.util.Collections;
@@ -58,6 +59,7 @@ class TraceImpl implements TraceInternal {
}
@Override
+ @JsonValue
public synchronized List<Span> getSpans() {
if (!finished) {
tracer.reportError("Cannot get spans, trace is not finished yet: %s", this);

ADD
dd-trace/src/test/groovy/datadog/trace/tracer/JsonSpan.groovy
+package datadog.trace.tracer
+
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonProperty
+import groovy.transform.EqualsAndHashCode
+
+/**
+ * Helper class to parse serialized span to verify serialization logic
+ */
+@EqualsAndHashCode
+class JsonSpan {
+ @JsonProperty("trace_id")
+ BigInteger traceId
+ @JsonProperty("parent_id")
+ BigInteger parentId
+ @JsonProperty("span_id")
+ BigInteger spanId
+
+ @JsonProperty("start")
+ long start
+ @JsonProperty("duration")
+ long duration
+
+ @JsonProperty("service")
+ String service
+ @JsonProperty("resource")
+ String resource
+ @JsonProperty("type")
+ String type
+ @JsonProperty("name")
+ String name
+
+ @JsonProperty("error")
+ boolean error
+
+ @JsonProperty("meta")
+ Map<String, String> meta
+
+ @JsonCreator
+ JsonSpan() {}
+
+ JsonSpan(Span span) {
+ traceId = new BigInteger(span.getContext().getTraceId())
+ parentId = new BigInteger(span.getContext().getParentId())
+ spanId = new BigInteger(span.getContext().getSpanId())
+
+ start = span.getStartTimestamp().getTime()
+ duration = span.getDuration()
+
+ service = span.getService()
+ resource = span.getResource()
+ type = span.getType()
+ name = span.getName()
+
+ error = span.isErrored()
+
+ meta = span.getMeta()
+ }
+}

MODIFY
dd-trace/src/test/groovy/datadog/trace/tracer/SpanImplTest.groovy
dd-trace/src/test/groovy/datadog/trace/tracer/SpanImplTest.groovy
package datadog.trace.tracer
+
+import com.fasterxml.jackson.databind.ObjectMapper
import datadog.trace.api.DDTags
import spock.lang.Specification
@@ -8,6 +10,7 @@ class SpanImplTest extends Specification {
private static final String SERVICE_NAME = "service.name"
private static final String PARENT_TRACE_ID = "trace id"
private static final String PARENT_SPAN_ID = "span id"
+ private static final long START_TIME = 100
private static final long DURATION = 321
def interceptors = [Mock(name: "interceptor-1", Interceptor), Mock(name: "interceptor-2", Interceptor)]
@@ -20,6 +23,7 @@ class SpanImplTest extends Specification {
getSpanId() >> PARENT_SPAN_ID
}
def startTimestamp = Mock(Timestamp) {
+ getTime() >> START_TIME
getDuration() >> DURATION
getDuration(_) >> { args -> args[0] + DURATION }
}
@@ -27,6 +31,8 @@ class SpanImplTest extends Specification {
getTracer() >> tracer
}
+ ObjectMapper objectMapper = new ObjectMapper()
+
def "test setters and default values"() {
when: "create span"
def span = new SpanImpl(trace, parentContext, startTimestamp)
@@ -38,6 +44,9 @@ class SpanImplTest extends Specification {
span.getContext().getTraceId() == PARENT_TRACE_ID
span.getContext().getParentId() == PARENT_SPAN_ID
span.getContext().getSpanId() ==~ /\d+/
+ span.getTraceId() == PARENT_TRACE_ID
+ span.getParentId() == PARENT_SPAN_ID
+ span.getSpanId() == span.getContext().getSpanId()
span.getService() == SERVICE_NAME
span.getResource() == null
span.getType() == null
@@ -110,6 +119,19 @@ class SpanImplTest extends Specification {
"number.key" | 123
}
+ def "test getMeta"() {
+ setup:
+ def span = new SpanImpl(trace, parentContext, startTimestamp)
+
+ when:
+ span.setMeta("number.key", 123)
+ span.setMeta("string.key", "meta string")
+ span.setMeta("boolean.key", true)
+
+ then:
+ span.getMeta() == ["number.key": "123", "string.key": "meta string", "boolean.key": "true"]
+ }
+
def "test meta setter on finished span for #key"() {
setup: "create span"
def span = new SpanImpl(trace, parentContext, startTimestamp)
@@ -289,4 +311,64 @@ class SpanImplTest extends Specification {
then:
thrown TraceException
}
+
+ def "test JSON rendering"() {
+ setup: "create span"
+ def parentContext = new SpanContextImpl("123", "456", "789")
+ def span = new SpanImpl(trace, parentContext, startTimestamp)
+ span.setResource("test resource")
+ span.setType("test type")
+ span.setName("test name")
+ span.setMeta("number.key", 123)
+ span.setMeta("string.key", "meta string")
+ span.setMeta("boolean.key", true)
+ span.finish()
+
+ when: "convert to JSON"
+ def string = objectMapper.writeValueAsString(span)
+ def parsedSpan = objectMapper.readerFor(JsonSpan).readValue(string)
+
+ then:
+ parsedSpan == new JsonSpan(span)
+ }
+
+ def "test JSON rendering with throwable"() {
+ setup: "create span"
+ def parentContext = new SpanContextImpl("123", "456", "789")
+ def span = new SpanImpl(trace, parentContext, startTimestamp)
+ span.attachThrowable(new RuntimeException("test"))
+ span.finish()
+
+ when: "convert to JSON"
+ def string = objectMapper.writeValueAsString(span)
+ def parsedSpan = objectMapper.readerFor(JsonSpan).readValue(string)
+
+ then:
+ parsedSpan == new JsonSpan(span)
+ }
+
+ def "test JSON rendering with big ID values"() {
+ setup: "create span"
+ def parentContext = new SpanContextImpl(
+ new BigInteger(2).pow(64).subtract(1).toString(),
+ "123",
+ new BigInteger(2).pow(64).subtract(2).toString())
+ def span = new SpanImpl(trace, parentContext, startTimestamp)
+ span.finish()
+
+ when: "convert to JSON"
+ def string = objectMapper.writeValueAsString(span)
+ def parsedSpan = objectMapper.readValue(string, JsonSpan)
+
+ then:
+ parsedSpan == new JsonSpan(span)
+
+ when:
+ def json = objectMapper.readTree(string)
+
+ then: "make sure ids rendered as number"
+ json.get("trace_id").isNumber()
+ json.get("parent_id").isNumber()
+ json.get("span_id").isNumber()
+ }
}

MODIFY
dd-trace/src/test/groovy/datadog/trace/tracer/TimestampTest.groovy
dd-trace/src/test/groovy/datadog/trace/tracer/TimestampTest.groovy
package datadog.trace.tracer
+import com.fasterxml.jackson.databind.ObjectMapper
import nl.jqno.equalsverifier.EqualsVerifier
import nl.jqno.equalsverifier.Warning
import spock.lang.Specification
@@ -18,6 +19,8 @@ class TimestampTest extends Specification {
getTracer() >> tracer
}
+ ObjectMapper objectMapper = new ObjectMapper()
+
def "test getter"() {
when:
def timestamp = new Timestamp(clock)
@@ -26,6 +29,20 @@ class TimestampTest extends Specification {
timestamp.getClock() == clock
}
+ def "test getTime"() {
+ setup:
+ clock.nanoTicks() >> CLOCK_NANO_TICKS
+ clock.getStartTimeNano() >> CLOCK_START_TIME
+ clock.getStartNanoTicks() >> CLOCK_START_NANO_TICKS
+ def timestamp = new Timestamp(clock)
+
+ when:
+ def time = timestamp.getTime()
+
+ then:
+ time == 300
+ }
+
def "test getDuration with literal finish time"() {
setup:
clock.nanoTicks() >> CLOCK_NANO_TICKS
@@ -62,6 +79,7 @@ class TimestampTest extends Specification {
def "test getDuration with current time"() {
setup:
+ clock.createCurrentTimestamp() >> { new Timestamp(clock) }
clock.nanoTicks() >> CLOCK_START_NANO_TICKS >> FINISH_NANO_TICKS
def timestamp = new Timestamp(clock)
@@ -102,4 +120,19 @@ class TimestampTest extends Specification {
then:
noExceptionThrown()
}
+
+ def "test JSON rendering"() {
+ setup:
+ clock.nanoTicks() >> CLOCK_NANO_TICKS
+ clock.getStartTimeNano() >> CLOCK_START_TIME
+ clock.getStartNanoTicks() >> CLOCK_START_NANO_TICKS
+ def timestamp = new Timestamp(clock)
+
+ when:
+ def string = objectMapper.writeValueAsString(timestamp)
+
+
+ then:
+ string == "300"
+ }
}

MODIFY
dd-trace/src/test/groovy/datadog/trace/tracer/TraceImplTest.groovy
dd-trace/src/test/groovy/datadog/trace/tracer/TraceImplTest.groovy
package datadog.trace.tracer
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.ObjectMapper
import datadog.trace.tracer.sampling.Sampler
import datadog.trace.tracer.writer.Writer
import spock.lang.Specification
-
class TraceImplTest extends Specification {
private static final String SERVICE_NAME = "service.name"
@@ -35,6 +36,8 @@ class TraceImplTest extends Specification {
}
def startTimestamp = Mock(Timestamp)
+ ObjectMapper objectMapper = new ObjectMapper()
+
def "test getters"() {
when:
def trace = new TraceImpl(tracer, parentContext, startTimestamp)
@@ -456,4 +459,35 @@ class TraceImplTest extends Specification {
then: "error is reported"
thrown TraceException
}
+
+ def "test JSON rendering"() {
+ setup: "create trace"
+ def clock = new Clock(tracer)
+ def parentContext = new SpanContextImpl("123", "456", "789")
+ def trace = new TraceImpl(tracer, parentContext, clock.createCurrentTimestamp())
+ trace.getRootSpan().setResource("test resource")
+ trace.getRootSpan().setType("test type")
+ trace.getRootSpan().setName("test name")
+ trace.getRootSpan().setMeta("number.key", 123)
+ trace.getRootSpan().setMeta("string.key", "meta string")
+ trace.getRootSpan().setMeta("boolean.key", true)
+
+ def childSpan = trace.createSpan(trace.getRootSpan().getContext())
+ childSpan.setResource("child span test resource")
+ childSpan.setType("child span test type")
+ childSpan.setName("child span test name")
+ childSpan.setMeta("child.span.number.key", 123)
+ childSpan.setMeta("child.span.string.key", "meta string")
+ childSpan.setMeta("child.span.boolean.key", true)
+ childSpan.finish()
+
+ trace.getRootSpan().finish()
+
+ when: "convert to JSON"
+ def string = objectMapper.writeValueAsString(trace)
+ def parsedTrace = objectMapper.readValue(string, new TypeReference<List<JsonSpan>>() {})
+
+ then:
+ parsedTrace == [new JsonSpan(childSpan), new JsonSpan(trace.getRootSpan())]
+ }
}

Initial support for JSON rendering in new API