Initial support for JSON rendering in new API

This commit is contained in:
Nikolay Martynov 2019-01-03 15:15:40 -05:00
parent 20b134e356
commit ed29969cb8
7 changed files with 289 additions and 6 deletions

View File

@ -1,8 +1,18 @@
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));
}
}
}

View File

@ -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);
}

View File

@ -1,5 +1,6 @@
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);

View File

@ -0,0 +1,59 @@
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()
}
}

View File

@ -1,5 +1,7 @@
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()
}
}

View File

@ -1,5 +1,6 @@
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"
}
}

View File

@ -1,10 +1,11 @@
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())]
}
}