Skip to content

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
40 changes: 40 additions & 0 deletions dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper;
import datadog.trace.bootstrap.instrumentation.api.DDSpanEvent;
import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities;
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import javax.annotation.Nonnull;
Expand All @@ -47,6 +50,7 @@
*/
public class DDSpan implements AgentSpan, CoreSpan<DDSpan>, AttachableWrapper {
private static final Logger log = LoggerFactory.getLogger(DDSpan.class);
private static final String SPAN_EVENTS = "_dd.span_events";

static DDSpan create(
final String instrumentationName,
Expand Down Expand Up @@ -109,6 +113,8 @@ static DDSpan create(

private final List<AgentSpanLink> links;

private final List<DDSpanEvent> events = new ArrayList<>();

/**
* Spans should be constructed using the builder, not by calling the constructor directly.
*
Expand Down Expand Up @@ -156,6 +162,17 @@ private void finishAndAddToTrace(final long durationNano) {

@Override
public void finish() {
if (events != null && !events.isEmpty()) {
StringBuilder eventsJson = new StringBuilder("[");
for (int i = 0; i < events.size(); i++) {
if (i > 0) {
eventsJson.append(",");
}
eventsJson.append(events.get(i).toJson());
}
eventsJson.append(']');
setTag(SPAN_EVENTS, eventsJson.toString());
}
if (!externalClock) {
// no external clock was used, so we can rely on nano time
finishAndAddToTrace(context.getTraceCollector().getCurrentTimeNano() - startTimeNano);
Expand Down Expand Up @@ -856,4 +873,27 @@ public boolean isOutbound() {
Object spanKind = context.getTag(Tags.SPAN_KIND);
return Tags.SPAN_KIND_CLIENT.equals(spanKind) || Tags.SPAN_KIND_PRODUCER.equals(spanKind);
}

public AgentSpan addEvent(String name) {
return addEvent(name, null);
}

public AgentSpan addEvent(String name, Map<String, Object> attributes) {
if (name != null) {
events.add(new DDSpanEvent(name, attributes));
Copy link
Contributor

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current timestamp.

}
return this;
}

public AgentSpan addEvent(
String name, Map<String, Object> attributes, long timestamp, TimeUnit unit) {
if (name != null) {
events.add(new DDSpanEvent(name, attributes, unit.toNanos(timestamp)));
}
return this;
}

public List<DDSpanEvent> getEvents() {
return events;
}
}
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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()
}
}
Loading