Skip to content

Commit a754504

Browse files
authored
feat: enable top-level span events (#12645)
Enable encoding top-level span events according to [RFC](https://docs.google.com/document/d/1cVod_VI7Yruq8U9dfMRFJd7npDu-uBpste2IB04GyaQ/edit?tab=t.0#heading=h.z8rsw69ccced) for native span events implementation. If `DD_TRACE_NATIVE_SPAN_EVENTS` is True, the encoder version should be `v0.4` and we should encode span events as a top level field. - Change type of SpanEvent attributes from `_JSONType` to `AttributeValue` (matching OTel). microbenchmark results (normal span events vs native span events): ``` encoder-one-trace-span-event 2.38 ms: 1.03x slower encoder-one-trace-top-level-span-event 629 us: 3.67x faster ``` ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent 19a1625 commit a754504

File tree

9 files changed

+266
-24
lines changed

9 files changed

+266
-24
lines changed

ddtrace/_trace/span.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ddtrace._trace._span_pointer import _SpanPointer
2323
from ddtrace._trace._span_pointer import _SpanPointerDirection
2424
from ddtrace._trace.context import Context
25+
from ddtrace._trace.types import _AttributeValueType
2526
from ddtrace._trace.types import _MetaDictType
2627
from ddtrace._trace.types import _MetricDictType
2728
from ddtrace._trace.types import _TagNameType
@@ -53,7 +54,6 @@
5354
from ddtrace.internal.logger import get_logger
5455
from ddtrace.internal.sampling import SamplingMechanism
5556
from ddtrace.internal.sampling import set_sampling_decision_maker
56-
from ddtrace.settings._config import _JSONType
5757

5858

5959
_NUMERIC_TAGS = (_ANALYTICS_SAMPLE_RATE_KEY,)
@@ -63,16 +63,16 @@ class SpanEvent:
6363
__slots__ = ["name", "attributes", "time_unix_nano"]
6464

6565
def __init__(
66-
self, name: str, attributes: Optional[Dict[str, _JSONType]] = None, time_unix_nano: Optional[int] = None
66+
self,
67+
name: str,
68+
attributes: Optional[Dict[str, _AttributeValueType]] = None,
69+
time_unix_nano: Optional[int] = None,
6770
):
6871
self.name: str = name
69-
if attributes is None:
70-
self.attributes = {}
71-
else:
72-
self.attributes = attributes
7372
if time_unix_nano is None:
7473
time_unix_nano = time_ns()
7574
self.time_unix_nano: int = time_unix_nano
75+
self.attributes: dict = attributes if attributes else {}
7676

7777
def __dict__(self):
7878
d = {"name": self.name, "time_unix_nano": self.time_unix_nano}
@@ -89,6 +89,12 @@ def __str__(self):
8989
attrs_str = ",".join(f"{k}:{v}" for k, v in self.attributes.items())
9090
return f"name={self.name} time={self.time_unix_nano} attributes={attrs_str}"
9191

92+
def __iter__(self):
93+
yield "name", self.name
94+
yield "time_unix_nano", self.time_unix_nano
95+
if self.attributes:
96+
yield "attributes", self.attributes
97+
9298

9399
log = get_logger(__name__)
94100

@@ -180,7 +186,6 @@ def __init__(
180186
if config._raise:
181187
raise TypeError("parent_id must be an integer")
182188
return
183-
184189
self.name = name
185190
self.service = service
186191
self._resource = [resource or name]
@@ -496,7 +501,7 @@ def get_metric(self, key: _TagNameType) -> Optional[NumericType]:
496501
return self._metrics.get(key)
497502

498503
def _add_event(
499-
self, name: str, attributes: Optional[Dict[str, _JSONType]] = None, timestamp: Optional[int] = None
504+
self, name: str, attributes: Optional[Dict[str, _AttributeValueType]] = None, timestamp: Optional[int] = None
500505
) -> None:
501506
self._events.append(SpanEvent(name, attributes, timestamp))
502507

@@ -587,7 +592,6 @@ def set_exc_info(
587592
return
588593

589594
self.error = 1
590-
591595
tb = self._get_traceback(exc_type, exc_val, exc_tb, limit=limit)
592596

593597
# readable version of type (e.g. exceptions.ZeroDivisionError)
@@ -614,7 +618,7 @@ def set_exc_info(
614618
def record_exception(
615619
self,
616620
exception: BaseException,
617-
attributes: Optional[Dict[str, _JSONType]] = None,
621+
attributes: Optional[Dict[str, _AttributeValueType]] = None,
618622
timestamp: Optional[int] = None,
619623
escaped=False,
620624
) -> None:

ddtrace/_trace/types.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Dict
2+
from typing import Sequence
23
from typing import Text
34
from typing import Union
45

@@ -8,3 +9,13 @@
89
_TagNameType = Union[Text, bytes]
910
_MetaDictType = Dict[_TagNameType, Text]
1011
_MetricDictType = Dict[_TagNameType, NumericType]
12+
_AttributeValueType = Union[
13+
str,
14+
bool,
15+
int,
16+
float,
17+
Sequence[str],
18+
Sequence[bool],
19+
Sequence[int],
20+
Sequence[float],
21+
]

ddtrace/internal/_encoding.pyx

+157-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ from ..constants import _ORIGIN_KEY as ORIGIN_KEY
2323
from .constants import SPAN_LINKS_KEY
2424
from .constants import SPAN_EVENTS_KEY
2525
from .constants import MAX_UINT_64BITS
26+
from ..settings._agent import config as agent_config
2627

2728

2829
DEF MSGPACK_ARRAY_LENGTH_PREFIX_SIZE = 5
@@ -49,6 +50,7 @@ cdef extern from "pack.h":
4950
int msgpack_pack_bin(msgpack_packer* pk, size_t l)
5051
int msgpack_pack_raw_body(msgpack_packer* pk, char* body, size_t l)
5152
int msgpack_pack_unicode(msgpack_packer* pk, object o, long long limit)
53+
int msgpack_pack_uint8(msgpack_packer* pk, stdint.uint8_t d)
5254
int msgpack_pack_uint32(msgpack_packer* pk, stdint.uint32_t d)
5355
int msgpack_pack_uint64(msgpack_packer* pk, stdint.uint64_t d)
5456
int msgpack_pack_int32(msgpack_packer* pk, stdint.int32_t d)
@@ -101,6 +103,10 @@ cdef inline int pack_bytes(msgpack_packer *pk, char *bs, Py_ssize_t l):
101103
ret = msgpack_pack_raw_body(pk, bs, l)
102104
return ret
103105

106+
cdef inline int pack_bool(msgpack_packer *pk, bint n) except? -1:
107+
if n:
108+
return msgpack_pack_true(pk)
109+
return msgpack_pack_false(pk)
104110

105111
cdef inline int pack_number(msgpack_packer *pk, object n) except? -1:
106112
if n is None:
@@ -548,6 +554,11 @@ cdef class MsgpackEncoderBase(BufferedEncoder):
548554

549555

550556
cdef class MsgpackEncoderV04(MsgpackEncoderBase):
557+
cdef bint top_level_span_event_encoding
558+
559+
def __cinit__(self, size_t max_size, size_t max_item_size):
560+
self.top_level_span_event_encoding = agent_config.trace_native_span_events
561+
551562
cpdef flush(self):
552563
with self._lock:
553564
try:
@@ -611,6 +622,57 @@ cdef class MsgpackEncoderV04(MsgpackEncoderBase):
611622
return ret
612623
return 0
613624

625+
cdef inline int _pack_span_events(self, list span_events) except? -1:
626+
cdef int ret
627+
cdef int L
628+
cdef str attr_k
629+
cdef object attr_v
630+
cdef object event
631+
ret = msgpack_pack_array(&self.pk, len(span_events))
632+
if ret != 0:
633+
return ret
634+
635+
for event in span_events:
636+
L = 2 + bool(event.attributes)
637+
ret = msgpack_pack_map(&self.pk, L)
638+
if ret != 0:
639+
return ret
640+
641+
ret = pack_bytes(&self.pk, <char*> b"name", 4)
642+
if ret != 0:
643+
return ret
644+
645+
ret = pack_text(&self.pk, event.name)
646+
if ret != 0:
647+
return ret
648+
649+
ret = pack_bytes(&self.pk, <char*> b"time_unix_nano", 14)
650+
if ret != 0:
651+
return ret
652+
653+
ret = pack_number(&self.pk, event.time_unix_nano)
654+
if ret != 0:
655+
return ret
656+
657+
if event.attributes:
658+
ret = pack_bytes(&self.pk, <char*> b"attributes", 10)
659+
if ret != 0:
660+
return ret
661+
662+
ret = msgpack_pack_map(&self.pk, len(event.attributes))
663+
if ret != 0:
664+
return ret
665+
666+
for attr_k, attr_v in event.attributes.items():
667+
ret = pack_text(&self.pk, attr_k)
668+
if ret != 0:
669+
return ret
670+
671+
ret = self.pack_span_event_attributes(attr_v)
672+
if ret != 0:
673+
return ret
674+
return ret
675+
614676
cdef inline int _pack_meta(self, object meta, char *dd_origin, str span_events) except? -1:
615677
cdef Py_ssize_t L
616678
cdef int ret
@@ -679,13 +741,20 @@ cdef class MsgpackEncoderV04(MsgpackEncoderBase):
679741
has_error = <bint> (span.error != 0)
680742
has_span_type = <bint> (span.span_type is not None)
681743
has_span_events = <bint> (len(span._events) > 0)
682-
has_meta = <bint> (len(span._meta) > 0 or dd_origin is not NULL or has_span_events)
683744
has_metrics = <bint> (len(span._metrics) > 0)
684745
has_parent_id = <bint> (span.parent_id is not None)
685746
has_links = <bint> (len(span._links) > 0)
686747
has_meta_struct = <bint> (len(span._meta_struct) > 0)
748+
has_meta = <bint> (
749+
len(span._meta) > 0
750+
or dd_origin is not NULL
751+
or (not self.top_level_span_event_encoding and has_span_events)
752+
)
687753

754+
# do not include in meta
688755
L = 7 + has_span_type + has_meta + has_metrics + has_error + has_parent_id + has_links + has_meta_struct
756+
if self.top_level_span_event_encoding:
757+
L += has_span_events
689758

690759
ret = msgpack_pack_map(&self.pk, L)
691760

@@ -771,13 +840,21 @@ cdef class MsgpackEncoderV04(MsgpackEncoderBase):
771840
if ret != 0:
772841
return ret
773842

843+
if has_span_events and self.top_level_span_event_encoding:
844+
ret = pack_bytes(&self.pk, <char *> b"span_events", 11)
845+
if ret != 0:
846+
return ret
847+
ret = self._pack_span_events(span._events)
848+
if ret != 0:
849+
return ret
850+
774851
if has_meta:
775852
ret = pack_bytes(&self.pk, <char *> b"meta", 4)
776853
if ret != 0:
777854
return ret
778855

779856
span_events = ""
780-
if has_span_events:
857+
if has_span_events and not self.top_level_span_event_encoding:
781858
span_events = json_dumps([vars(event)() for event in span._events])
782859
ret = self._pack_meta(span._meta, <char *> dd_origin, span_events)
783860
if ret != 0:
@@ -812,6 +889,84 @@ cdef class MsgpackEncoderV04(MsgpackEncoderBase):
812889

813890
return ret
814891

892+
cdef int pack_span_event_attributes(self, object attr, int depth=0) except ? -1:
893+
cdef int ret
894+
cdef object elt
895+
896+
ret = msgpack_pack_map(&self.pk, 2)
897+
if ret != 0:
898+
return ret
899+
ret = pack_bytes(&self.pk, <char*> b"type", 4)
900+
if ret != 0:
901+
return ret
902+
903+
if isinstance(attr, str):
904+
ret = msgpack_pack_uint8(&self.pk, 0)
905+
if ret != 0:
906+
return ret
907+
ret = pack_bytes(&self.pk, <char*> b"string_value", 12)
908+
if ret != 0:
909+
return ret
910+
ret = pack_text(&self.pk, attr)
911+
if ret != 0:
912+
return ret
913+
elif isinstance(attr, bool):
914+
ret = msgpack_pack_uint8(&self.pk, 1)
915+
if ret != 0:
916+
return ret
917+
ret = pack_bytes(&self.pk, <char*> b"bool_value", 10)
918+
if ret != 0:
919+
return ret
920+
ret = pack_bool(&self.pk, attr)
921+
if ret != 0:
922+
return ret
923+
elif isinstance(attr, int):
924+
ret = msgpack_pack_uint8(&self.pk, 2)
925+
if ret != 0:
926+
return ret
927+
ret = pack_bytes(&self.pk, <char*> b"int_value", 9)
928+
if ret != 0:
929+
return ret
930+
ret = pack_number(&self.pk, attr)
931+
if ret != 0:
932+
return ret
933+
elif isinstance(attr, float):
934+
ret = msgpack_pack_uint8(&self.pk, 3)
935+
if ret != 0:
936+
return ret
937+
ret = pack_bytes(&self.pk, <char*> b"double_value", 12)
938+
if ret != 0:
939+
return ret
940+
ret = pack_number(&self.pk, attr)
941+
if ret != 0:
942+
return ret
943+
elif isinstance(attr, list):
944+
if depth != 0:
945+
raise ValueError("Nested list found; cannot encode")
946+
ret = msgpack_pack_uint8(&self.pk, 4)
947+
if ret != 0:
948+
return ret
949+
ret = pack_bytes(&self.pk, <char*> b"array_value", 11)
950+
if ret != 0:
951+
return ret
952+
ret = msgpack_pack_map(&self.pk, 1)
953+
if ret != 0:
954+
return ret
955+
ret = pack_bytes(&self.pk, <char*> b"values", 6)
956+
if ret != 0:
957+
return ret
958+
ret = msgpack_pack_array(&self.pk, len(attr))
959+
if ret != 0:
960+
return ret
961+
962+
for elt in attr:
963+
ret = self.pack_span_event_attributes(elt, depth+1)
964+
if ret != 0:
965+
return ret
966+
else:
967+
raise ValueError(f"Unsupported type for SpanEvent attribute: {type(attr)}")
968+
969+
return ret
815970

816971
cdef class MsgpackEncoderV05(MsgpackEncoderBase):
817972
cdef MsgpackStringTable _st

ddtrace/internal/encoding.py

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Optional # noqa:F401
77
from typing import Tuple # noqa:F401
88

9+
from ..settings._agent import config as agent_config # noqa:F401
910
from ._encoding import ListStringTable
1011
from ._encoding import MsgpackEncoderV04
1112
from ._encoding import MsgpackEncoderV05
@@ -86,6 +87,9 @@ def _span_to_dict(span):
8687
if span._links:
8788
d["span_links"] = [link.to_dict() for link in span._links]
8889

90+
if span._events and agent_config.trace_native_span_events:
91+
d["span_events"] = [dict(event) for event in span._events]
92+
8993
return d
9094

9195

ddtrace/internal/writer/writer.py

+5
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,11 @@ def __init__(
468468
default_api_version = "v0.4"
469469

470470
self._api_version = api_version or config._trace_api or default_api_version
471+
472+
if agent_config.trace_native_span_events:
473+
log.warning("Setting api version to v0.4; DD_TRACE_NATIVE_SPAN_EVENTS is not compatible with v0.5")
474+
self._api_version = "v0.4"
475+
471476
if is_windows and self._api_version == "v0.5":
472477
raise RuntimeError(
473478
"There is a known compatibility issue with v0.5 API and Windows, "

ddtrace/settings/_agent.py

+8
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ class AgentConfig(DDConfig):
141141
help_type="Int",
142142
help="Stores the port of the agent",
143143
)
144+
145+
trace_native_span_events = DDConfig.v(
146+
bool,
147+
"trace_native_span_events",
148+
default=False,
149+
help_type="Boolean",
150+
help="Stores whether native span events are enabled",
151+
)
144152
# Effective trace agent URL (this is the one that will be used)
145153
trace_agent_url = DDConfig.d(str, _derive_trace_url)
146154
# Effective DogStatsD URL (this is the one that will be used)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features:
3+
- |
4+
Adds configuration for encoding span events as a top-level field in v0.4 payloads and introduces environment variable ``DD_TRACE_NATIVE_SPAN_EVENTS`` (disabled by default).
5+
This requires agent version 7.63.0 or later.

tests/telemetry/test_writer.py

+1
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
483483
{"name": "DD_TRACE_LOG_FILE_SIZE_BYTES", "origin": "default", "value": 15728640},
484484
{"name": "DD_TRACE_LOG_STREAM_HANDLER", "origin": "default", "value": True},
485485
{"name": "DD_TRACE_METHODS", "origin": "default", "value": None},
486+
{"name": "DD_TRACE_NATIVE_SPAN_EVENTS", "origin": "default", "value": False},
486487
{"name": "DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP", "origin": "env_var", "value": ".*"},
487488
{"name": "DD_TRACE_OTEL_ENABLED", "origin": "env_var", "value": True},
488489
{"name": "DD_TRACE_PARTIAL_FLUSH_ENABLED", "origin": "env_var", "value": False},

0 commit comments

Comments
 (0)