From ed29969cb8ad5996b8b5da485463b5a2b0cc835f Mon Sep 17 00:00:00 2001 From: Nikolay Martynov Date: Thu, 3 Jan 2019 15:15:40 -0500 Subject: [PATCH] Initial support for JSON rendering in new API --- .../java/datadog/trace/tracer/SpanImpl.java | 69 ++++++++++++++++ .../java/datadog/trace/tracer/Timestamp.java | 14 ++-- .../java/datadog/trace/tracer/TraceImpl.java | 2 + .../datadog/trace/tracer/JsonSpan.groovy | 59 +++++++++++++ .../datadog/trace/tracer/SpanImplTest.groovy | 82 +++++++++++++++++++ .../datadog/trace/tracer/TimestampTest.groovy | 33 ++++++++ .../datadog/trace/tracer/TraceImplTest.groovy | 36 +++++++- 7 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 dd-trace/src/test/groovy/datadog/trace/tracer/JsonSpan.groovy diff --git a/dd-trace/src/main/java/datadog/trace/tracer/SpanImpl.java b/dd-trace/src/main/java/datadog/trace/tracer/SpanImpl.java index 3a21cc03ed..d91e61b8c4 100644 --- a/dd-trace/src/main/java/datadog/trace/tracer/SpanImpl.java +++ b/dd-trace/src/main/java/datadog/trace/tracer/SpanImpl.java @@ -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 getMeta() { + final Map result = new HashMap<>(meta.size()); + for (final Map.Entry 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 { + + 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)); + } + } } diff --git a/dd-trace/src/main/java/datadog/trace/tracer/Timestamp.java b/dd-trace/src/main/java/datadog/trace/tracer/Timestamp.java index c4e4963fe2..642659418c 100644 --- a/dd-trace/src/main/java/datadog/trace/tracer/Timestamp.java +++ b/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); } diff --git a/dd-trace/src/main/java/datadog/trace/tracer/TraceImpl.java b/dd-trace/src/main/java/datadog/trace/tracer/TraceImpl.java index 44b4b8ef24..a79ce3722f 100644 --- a/dd-trace/src/main/java/datadog/trace/tracer/TraceImpl.java +++ b/dd-trace/src/main/java/datadog/trace/tracer/TraceImpl.java @@ -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 getSpans() { if (!finished) { tracer.reportError("Cannot get spans, trace is not finished yet: %s", this); diff --git a/dd-trace/src/test/groovy/datadog/trace/tracer/JsonSpan.groovy b/dd-trace/src/test/groovy/datadog/trace/tracer/JsonSpan.groovy new file mode 100644 index 0000000000..c6a9948343 --- /dev/null +++ b/dd-trace/src/test/groovy/datadog/trace/tracer/JsonSpan.groovy @@ -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 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() + } +} diff --git a/dd-trace/src/test/groovy/datadog/trace/tracer/SpanImplTest.groovy b/dd-trace/src/test/groovy/datadog/trace/tracer/SpanImplTest.groovy index aad6672066..9e38feef40 100644 --- a/dd-trace/src/test/groovy/datadog/trace/tracer/SpanImplTest.groovy +++ b/dd-trace/src/test/groovy/datadog/trace/tracer/SpanImplTest.groovy @@ -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() + } } diff --git a/dd-trace/src/test/groovy/datadog/trace/tracer/TimestampTest.groovy b/dd-trace/src/test/groovy/datadog/trace/tracer/TimestampTest.groovy index af1ed553cd..dcbad3de3c 100644 --- a/dd-trace/src/test/groovy/datadog/trace/tracer/TimestampTest.groovy +++ b/dd-trace/src/test/groovy/datadog/trace/tracer/TimestampTest.groovy @@ -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" + } } diff --git a/dd-trace/src/test/groovy/datadog/trace/tracer/TraceImplTest.groovy b/dd-trace/src/test/groovy/datadog/trace/tracer/TraceImplTest.groovy index 2547ed8339..e69f521aab 100644 --- a/dd-trace/src/test/groovy/datadog/trace/tracer/TraceImplTest.groovy +++ b/dd-trace/src/test/groovy/datadog/trace/tracer/TraceImplTest.groovy @@ -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>() {}) + + then: + parsedTrace == [new JsonSpan(childSpan), new JsonSpan(trace.getRootSpan())] + } }