-
Notifications
You must be signed in to change notification settings - Fork 301
Added span events to the DD Trace API #8585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
c6d2de6
0bf493c
d896cbb
1053c56
918a4b1
6a2ff8d
fd61266
f68d11a
d99b87c
a10ae0e
930e07a
572facc
4a2d5c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package datadog.trace.bootstrap.instrumentation.api | ||
|
||
import datadog.trace.api.time.SystemTimeSource | ||
import datadog.trace.api.time.TimeSource | ||
import datadog.trace.core.test.DDCoreSpecification | ||
import spock.lang.Shared | ||
|
||
class DDSpanEventTest extends DDCoreSpecification { | ||
@Shared | ||
def mockTimeSource = Mock(TimeSource) | ||
@Shared | ||
def defaultTimestamp = 1234567890000000L | ||
|
||
def setup() { | ||
mockTimeSource = Mock(TimeSource) // Create a fresh mock for each test | ||
DDSpanEvent.setTimeSource(mockTimeSource) | ||
} | ||
|
||
def cleanup() { | ||
DDSpanEvent.setTimeSource(SystemTimeSource.INSTANCE) | ||
} | ||
|
||
def "test event creation with current time"() { | ||
given: | ||
mockTimeSource.getCurrentTimeNanos() >> defaultTimestamp | ||
def name = "test-event" | ||
def attributes = ["key1": "value1", "key2": 123] | ||
|
||
when: | ||
def event = new DDSpanEvent(name, attributes) | ||
|
||
then: | ||
event.getName() == name | ||
event.getAttributes() == attributes | ||
event.getTimestampNanos() == defaultTimestamp | ||
} | ||
|
||
def "test event creation with explicit timestamp"() { | ||
given: | ||
def timestamp = 1742232412103000000L | ||
def name = "test-event" | ||
def attributes = ["key1": "value1", "key2": 123] | ||
|
||
when: | ||
def event = new DDSpanEvent(name, attributes, timestamp) | ||
|
||
then: | ||
0 * mockTimeSource.getCurrentTimeNanos() | ||
event.getName() == name | ||
event.getAttributes() == attributes | ||
event.getTimestampNanos() == timestamp | ||
} | ||
|
||
def "test event creation with null attributes"() { | ||
given: | ||
mockTimeSource.getCurrentTimeNanos() >> defaultTimestamp | ||
def name = "test-event" | ||
|
||
when: | ||
def event = new DDSpanEvent(name, null) | ||
|
||
then: | ||
event.getName() == name | ||
event.getAttributes() == null | ||
event.getTimestampNanos() == defaultTimestamp | ||
} | ||
|
||
def "test event creation with empty attributes"() { | ||
given: | ||
mockTimeSource.getCurrentTimeNanos() >> defaultTimestamp | ||
def name = "test-event" | ||
def attributes = [:] | ||
|
||
when: | ||
def event = new DDSpanEvent(name, attributes) | ||
|
||
then: | ||
event.getName() == name | ||
event.getAttributes() == attributes | ||
event.getTimestampNanos() == defaultTimestamp | ||
} | ||
|
||
def "test toJson with different attribute types"() { | ||
given: | ||
def timestamp = 1742232412103000000L | ||
def name = "test-event" | ||
def attributes = [ | ||
"string": "value", | ||
"number": 42, | ||
"boolean": true, | ||
"null": null | ||
] | ||
|
||
when: | ||
def event = new DDSpanEvent(name, attributes, timestamp) | ||
def json = event.toJson() | ||
|
||
then: | ||
json == """{"time_unix_nano":${timestamp},"name":"${name}","attributes":{"string":"value","number":42,"boolean":true,"null":null}}""" | ||
} | ||
|
||
def "test toJson with null attributes"() { | ||
given: | ||
def timestamp = 1742232412103000000L | ||
def name = "test-event" | ||
|
||
when: | ||
def event = new DDSpanEvent(name, null, timestamp) | ||
def json = event.toJson() | ||
|
||
then: | ||
json == """{"time_unix_nano":${timestamp},"name":"${name}"}""" | ||
} | ||
|
||
def "test toJson with empty attributes"() { | ||
given: | ||
def timestamp = 1742232412103000000L | ||
def name = "test-event" | ||
def attributes = [:] | ||
|
||
when: | ||
def event = new DDSpanEvent(name, attributes, timestamp) | ||
def json = event.toJson() | ||
|
||
then: | ||
json == """{"time_unix_nano":${timestamp},"name":"${name}"}""" | ||
} | ||
|
||
def "test time source change"() { | ||
given: | ||
def newTimeSource = Mock(TimeSource) | ||
def timestamp = 1742232412103000000L | ||
newTimeSource.getCurrentTimeNanos() >> timestamp | ||
|
||
when: | ||
DDSpanEvent.setTimeSource(newTimeSource) | ||
def event = new DDSpanEvent("test", [:]) | ||
|
||
then: | ||
event.getTimestampNanos() == timestamp | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package datadog.trace.common.writer | ||
|
||
import com.squareup.moshi.Moshi | ||
import com.squareup.moshi.Types | ||
import datadog.trace.core.DDSpan | ||
import datadog.trace.core.test.DDCoreSpecification | ||
|
||
class DDSpanJsonAdapterTest extends DDCoreSpecification { | ||
def tracer = tracerBuilder().writer(new ListWriter()).build() | ||
def adapter = new Moshi.Builder() | ||
.add(DDSpanJsonAdapter.buildFactory(false)) | ||
.build() | ||
.adapter(Types.newParameterizedType(List, DDSpan)) | ||
def genericAdapter = new Moshi.Builder().build().adapter(Object) | ||
|
||
def "test span event serialization"() { | ||
setup: | ||
def span = tracer.buildSpan("test").start() | ||
def eventName = "test-event" | ||
def attributes = ["key1": "value1", "key2": 123] | ||
def timestamp = System.currentTimeMillis() | ||
|
||
when: "adding event with name and attributes" | ||
span.addEvent(eventName, attributes, timestamp, java.util.concurrent.TimeUnit.MILLISECONDS) | ||
span.finish() | ||
def jsonStr = adapter.toJson([span]) | ||
|
||
then: "event is serialized correctly in meta section" | ||
def actual = genericAdapter.fromJson(jsonStr) | ||
def actualSpan = actual[0] | ||
|
||
// Verify basic span fields | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test looks like an AI generated with unnecessary noise and duplicated code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had a hard time getting the test coverage reporter to be happy (it kept saying it was 50% coverage, when the tests pretty much covered everything), so I added more tests than I thought were needed, and I agree some of it seems like too much. I'll review these and see if can remove the noisy ones. |
||
actualSpan.service == span.getServiceName() | ||
actualSpan.name == span.getOperationName() | ||
actualSpan.resource == span.getResourceName() | ||
actualSpan.trace_id == span.getTraceId().toLong() | ||
actualSpan.span_id == span.getSpanId() | ||
actualSpan.parent_id == span.getParentId() | ||
actualSpan.start == span.getStartTime() | ||
actualSpan.duration == span.getDurationNano() | ||
actualSpan.error == span.getError() | ||
actualSpan.type == span.getSpanType() | ||
|
||
// Verify span events | ||
def actualEvents = actualSpan.meta["_dd.span_events"] | ||
def expectedEvent = "[{\"time_unix_nano\":${java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(timestamp)},\"name\":\"test-event\",\"attributes\":{\"key1\":\"value1\",\"key2\":123}}]" | ||
actualEvents.toString() == expectedEvent.toString() | ||
|
||
cleanup: | ||
tracer.close() | ||
} | ||
|
||
def "test multiple span events serialization"() { | ||
setup: | ||
def span = tracer.buildSpan("test").start() | ||
def timestamp1 = System.currentTimeMillis() | ||
def timestamp2 = timestamp1 + 1000 | ||
|
||
when: "adding multiple events" | ||
span.addEvent("event1", ["key1": "value1"], timestamp1, java.util.concurrent.TimeUnit.MILLISECONDS) | ||
span.addEvent("event2", ["key2": "value2"], timestamp2, java.util.concurrent.TimeUnit.MILLISECONDS) | ||
span.finish() | ||
def jsonStr = adapter.toJson([span]) | ||
|
||
then: "events are serialized correctly in meta section" | ||
def actual = genericAdapter.fromJson(jsonStr) | ||
def actualSpan = actual[0] | ||
|
||
// Verify basic span fields | ||
actualSpan.service == span.getServiceName() | ||
actualSpan.name == span.getOperationName() | ||
actualSpan.resource == span.getResourceName() | ||
actualSpan.trace_id == span.getTraceId().toLong() | ||
actualSpan.span_id == span.getSpanId() | ||
actualSpan.parent_id == span.getParentId() | ||
actualSpan.start == span.getStartTime() | ||
actualSpan.duration == span.getDurationNano() | ||
actualSpan.error == span.getError() | ||
actualSpan.type == span.getSpanType() | ||
|
||
// Verify span events | ||
def actualEvents = actualSpan.meta["_dd.span_events"] | ||
|
||
def expectedEvents = "[{\"time_unix_nano\":${java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(timestamp1)},\"name\":\"event1\",\"attributes\":{\"key1\":\"value1\"}},{\"time_unix_nano\":${java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(timestamp2)},\"name\":\"event2\",\"attributes\":{\"key2\":\"value2\"}}]" | ||
|
||
actualEvents.toString() == expectedEvents.toString() | ||
|
||
|
||
cleanup: | ||
tracer.close() | ||
} | ||
|
||
def "test span events added directly as tag"() { | ||
setup: | ||
def span = tracer.buildSpan("test").start() | ||
def eventsJson = """[ | ||
{ | ||
"time_unix_nano": 1234567890000000, | ||
"name": "manual-event", | ||
"attributes": { | ||
"foo": "bar", | ||
"count": 42 | ||
} | ||
} | ||
]""" | ||
|
||
when: "adding events JSON directly as a tag" | ||
span.setTag("_dd.span_events", eventsJson) | ||
span.finish() | ||
def jsonStr = adapter.toJson([span]) | ||
|
||
then: "events JSON is preserved exactly in meta section" | ||
def actual = genericAdapter.fromJson(jsonStr) | ||
def actualSpan = actual[0] | ||
|
||
// Verify basic span fields | ||
actualSpan.service == span.getServiceName() | ||
actualSpan.name == span.getOperationName() | ||
actualSpan.resource == span.getResourceName() | ||
actualSpan.trace_id == span.getTraceId().toLong() | ||
actualSpan.span_id == span.getSpanId() | ||
actualSpan.parent_id == span.getParentId() | ||
actualSpan.start == span.getStartTime() | ||
actualSpan.duration == span.getDurationNano() | ||
actualSpan.error == span.getError() | ||
actualSpan.type == span.getSpanType() | ||
|
||
// Verify span events | ||
def actualEvents = actualSpan.meta["_dd.span_events"] | ||
actualEvents.toString() == eventsJson | ||
|
||
cleanup: | ||
tracer.close() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does an event without a timestamp appear in the UI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current timestamp.