Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions manifests/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@ tests/:
test_process_discovery.py: missing_feature
test_sampling_delegation.py:
Test_Decisionless_Extraction: v2.4.0
test_span_events.py:
Test_Span_Events: missing_feature
test_span_links.py:
Test_Span_Links: v2.0.0
test_telemetry.py:
Expand Down
109 changes: 86 additions & 23 deletions tests/parametric/test_span_events.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import json
import pytest

from utils import scenarios, missing_feature, features, rfc
from utils import scenarios, features, rfc, irrelevant
from utils.parametric.spec.trace import find_span, find_trace


@rfc("https://docs.google.com/document/d/1cVod_VI7Yruq8U9dfMRFJd7npDu-uBpste2IB04GyaQ")
@scenarios.parametric
@features.span_events
@missing_feature(reason="Agent does not advertise native span events serialization support yet")
class Test_Span_Events:
def _test_span_with_event(self, _library_env, test_agent, test_library, retrieve_events):
"""Test adding a span event, with attributes, to an active span."""
def _test_span_with_native_event(self, _library_env, test_agent, test_library):
"""Test adding a span event, with attributes, to an active span.
Assumes native format where all values are serialized according to their original type.
"""
time0 = 123450
name0 = "event_name"
attributes0 = {
Expand Down Expand Up @@ -39,12 +40,12 @@ def _test_span_with_event(self, _library_env, test_agent, test_library, retrieve
assert len(trace) == 1
span = find_span(trace, s.span_id)

span_events = retrieve_events(span)
span_events = span["span_events"]

assert len(span_events) == 2

event = span_events[0]
assert event["time_unix_nano"] == time0 * 1000
assert event["time_unix_nano"] == time0 * 1000000000
assert event["name"] == name0
assert event["attributes"].get("string") == {"type": 0, "string_value": "bar"}
assert event["attributes"].get("bool") == {"type": 1, "bool_value": True}
Expand All @@ -53,61 +54,123 @@ def _test_span_with_event(self, _library_env, test_agent, test_library, retrieve

assert event["attributes"].get("str_arr") == {
"type": 4,
"array_value": {"type": 0, "string_value": ["a", "b", "c"]},
"array_value": [
{"type": 0, "string_value": "a"},
{"type": 0, "string_value": "b"},
{"type": 0, "string_value": "c"},
],
}
assert event["attributes"].get("bool_arr") == {
"type": 4,
"array_value": {"type": 1, "bool_value": [True, False]},
"array_value": [{"type": 1, "bool_value": True}, {"type": 1, "bool_value": False}],
}
assert event["attributes"].get("int_arr") == {
"type": 4,
"array_value": {"type": 2, "int_value": [5, 6]},
"array_value": [{"type": 2, "int_value": 5}, {"type": 2, "int_value": 6}],
}
assert event["attributes"].get("double_arr") == {
"type": 4,
"array_value": {"type": 3, "double_value": [1.1, 2.2]},
"array_value": [{"type": 3, "double_value": 1.1}, {"type": 3, "double_value": 2.2}],
}

event = span_events[1]
assert event["time_unix_nano"] == time1 * 1000
assert event["time_unix_nano"] == time1 * 1000000000
assert event["name"] == name1
assert event["attributes"].get("bool") == {"type": 1, "bool_value": False}
assert event["attributes"].get("int") == {"type": 2, "int_value": 0}
assert isinstance(event["attributes"].get("int").get("int"), int)
assert isinstance(event["attributes"].get("int").get("int_value"), int)
assert event["attributes"].get("double") == {"type": 3, "double_value": 0.0}
assert isinstance(event["attributes"].get("double").get("double"), float)
assert isinstance(event["attributes"].get("double").get("double_value"), float)

def _test_span_with_meta_event(self, _library_env, test_agent, test_library):
"""Test adding a span event, with attributes, to an active span.
Assumes meta format where all values are strings.
"""
time0 = 123450
name0 = "event_name"
attributes0 = {
"string": "bar",
"bool": "true",
"int": "1",
"double": "2.3",
"str_arr": ["a", "b", "c"],
"bool_arr": ["true", "false"],
"int_arr": ["5", "6"],
"double_arr": ["1.1", "2.2"],
}

time1 = 123451
name1 = "other_event"
attributes1 = {"bool": "false", "int": "0", "double": "0.0"}

@pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.7"}])
with test_library, test_library.otel_start_span("test") as s:
s.add_event(name0, timestamp=time0, attributes=attributes0)
s.add_event(name1, timestamp=time1, attributes=attributes1)

traces = test_agent.wait_for_num_traces(1)

trace = find_trace(traces, s.trace_id)
assert len(trace) == 1
span = find_span(trace, s.span_id)

span_events = json.loads(span.get("meta", {}).get("events"))

assert len(span_events) == 2

event = span_events[0]
assert event["time_unix_nano"] == time0 * 1000000000
assert event["name"] == name0
assert event["attributes"].get("string") == "bar"
assert event["attributes"].get("bool") == "true"
assert event["attributes"].get("int") == "1"
assert event["attributes"].get("double") == "2.3"
assert event["attributes"].get("str_arr") == ["a", "b", "c"]
assert event["attributes"].get("bool_arr") == ["true", "false"]
assert event["attributes"].get("int_arr") == ["5", "6"]
assert event["attributes"].get("double_arr") == ["1.1", "2.2"]

event = span_events[1]
assert event["time_unix_nano"] == time1 * 1000000000
assert event["name"] == name1
assert event["attributes"].get("bool") == "false"
assert event["attributes"].get("int") == "0"
assert isinstance(event["attributes"].get("int"), str)
assert event["attributes"].get("double") == "0.0"
assert isinstance(event["attributes"].get("double"), str)

@irrelevant(library="ruby", reason="Does not support v0.7")
@pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.7", "DD_TRACE_NATIVE_SPAN_EVENTS": "1"}])
def test_span_with_event_v07(self, library_env, test_agent, test_library):
"""Test adding a span event in the v0.7 format, which support the native attribute representation."""

self._test_span_with_event(library_env, test_agent, test_library, lambda span: span["span_events"])
self._test_span_with_native_event(library_env, test_agent, test_library)

@pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.4"}])
@pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.4", "DD_TRACE_NATIVE_SPAN_EVENTS": "1"}])
def test_span_with_event_v04(self, library_env, test_agent, test_library):
"""Test adding a span event in the v0.4 format, which support the native attribute representation."""

self._test_span_with_event(library_env, test_agent, test_library, lambda span: span["span_events"])
self._test_span_with_native_event(library_env, test_agent, test_library)

@pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.5"}])
@irrelevant(library="ruby", reason="Does not support v0.5")
@pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.5", "DD_TRACE_NATIVE_SPAN_EVENTS": "1"}])
def test_span_with_event_v05(self, library_env, test_agent, test_library):
"""Test adding a span event in the v0.5 format, which does not support the native attribute representation.
Thus span events are serialized as span tags.
Thus span events are serialized as span tags, and attribute values all strings.
"""

self._test_span_with_event(
library_env, test_agent, test_library, lambda span: json.loads(span.get("meta", {}).get("events"))
)
self._test_span_with_meta_event(library_env, test_agent, test_library)

@pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.4", "DD_TRACE_NATIVE_SPAN_EVENTS": "1"}])
def test_span_with_invalid_event_attributes(self, library_env, test_agent, test_library):
"""Test adding a span event, with invalid attributes, to an active span.
Span events with invalid attributes should be discarded.
Valid attributes should be kept.
"""

with test_library, test_library.dd_start_span("test") as s:
s.add_event(
"name",
timestamp=123,
time_unix_nano=123,
attributes={
"int": 1,
"invalid_arr1": [1, "a"],
Expand Down
55 changes: 44 additions & 11 deletions utils/build/docker/ruby/parametric/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,23 @@ def to_json(*_args)
end
end

class TraceSpanAddEventsArgs
attr_accessor :span_id, :name, :timestamp, :attributes

def initialize(params)
@span_id = params['span_id']
@name = params['name']
@timestamp = params['timestamp']
@attributes = params['attributes']
end
end

class TraceSpanAddEventReturn
def to_json(*_args)
{}.to_json
end
end

def get_ddtrace_version
Gem::Version.new(Datadog::VERSION)
end
Expand Down Expand Up @@ -562,6 +579,8 @@ def call(env)
handle_trace_span_error(req, res)
when '/trace/span/add_link'
handle_trace_span_add_link(req, res)
when '/trace/span/add_event'
handle_trace_span_add_event(req, res)
when '/trace/otel/start_span'
handle_trace_otel_start_span(req, res)
when '/trace/otel/add_event'
Expand Down Expand Up @@ -708,6 +727,31 @@ def handle_trace_span_add_link(req, res)
res.write(TraceSpanAddLinkReturn.new.to_json)
end

def handle_trace_span_add_event(req, res)
args = TraceSpanAddEventsArgs.new(JSON.parse(req.body.read))
span = find_span(args.span_id)

# Create a new SpanEvent with the provided parameters
event = Datadog::Tracing::SpanEvent.new(
args.name,
attributes: args.attributes,
time_unix_nano: args.timestamp * 1000
)

# Add the event to the span's events array
span.span_events << event

res.write(TraceSpanAddEventReturn.new.to_json)
end

def handle_trace_otel_add_event(req, res)
args = OtelAddEventArgs.new(JSON.parse(req.body.read))

span = OTEL_SPANS[args.span_id]
span.add_event(args.name, attributes: args.attributes, timestamp: args.timestamp)
res.write(OtelAddEventReturn.new.to_json)
end

def handle_trace_crash(_req, res)
STDOUT.puts "Crashing server..."
Process.kill('SEGV', Process.pid)
Expand Down Expand Up @@ -748,17 +792,6 @@ def handle_trace_otel_start_span(req, res)
res.write(OtelStartSpanReturn.new(span_id_b10, t_id).to_json)
end

def handle_trace_otel_add_event(req, res)
args = OtelAddEventArgs.new(JSON.parse(req.body.read))
span = OTEL_SPANS[args.span_id]
span.add_event(
args.name,
timestamp: otel_correct_time(args.timestamp),
attributes: args.attributes
)
res.write(OtelAddEventReturn.new.to_json)
end

def handle_trace_otel_record_exception(req, res)
args = OtelRecordExceptionArgs.new(JSON.parse(req.body.read))

Expand Down
7 changes: 5 additions & 2 deletions utils/parametric/_library_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,13 @@ def span_add_link(self, span_id: int, parent_id: int, attributes: dict | None =
},
)

def span_add_event(self, span_id: int, name: str, timestamp: int, attributes: dict | None = None):
def span_add_event(self, span_id: int, name: str, time_unix_nano: int, attributes: dict | None = None):
self._session.post(
self._url("/trace/span/add_event"),
json={
"span_id": span_id,
"name": name,
"timestamp": timestamp,
"timestamp": time_unix_nano,
"attributes": attributes or {},
},
)
Expand Down Expand Up @@ -433,6 +433,9 @@ def set_error(self, typestr: str = "", message: str = "", stack: str = ""):
def add_link(self, parent_id: int, attributes: dict | None = None):
self._client.span_add_link(self.span_id, parent_id, attributes)

def add_event(self, name: str, time_unix_nano: int, attributes: dict | None = None):
self._client.span_add_event(self.span_id, name, time_unix_nano, attributes)

def finish(self):
self._client.finish_span(self.span_id)

Expand Down
Loading