Skip to content

Commit b1ab71f

Browse files
authored
unify span links (#7327)
1 parent 6ef34dd commit b1ab71f

7 files changed

Lines changed: 189 additions & 161 deletions

File tree

tests/test_distributed.py

Lines changed: 15 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import json
66
from utils import features, interfaces, scenarios, slow, weblog
7-
from utils.docker_fixtures.spec.trace import span_link_trace_id_equals
8-
from utils.dd_types import DataDogLibrarySpan, LibraryTraceFormat
7+
from utils.dd_types import DataDogSpanLink
98

109

1110
@scenarios.trace_propagation_style_w3c
@@ -56,20 +55,20 @@ def test_span_links_from_conflicting_contexts(self):
5655
trace = [
5756
span
5857
for _, trace, span in interfaces.library.get_spans(self.req, full_trace=True)
59-
if _retrieve_span_links(span) is not None
58+
if len(span.get_span_links()) != 0
6059
and trace.trace_id_equals(2)
6160
and span["parent_id"] == 10 # Only fetch the trace that is related to the header extractions
6261
]
6362

6463
assert len(trace) == 1
6564
span = trace[0]
66-
links = _retrieve_span_links(span)
65+
links = span.get_span_links()
6766
assert len(links) == 1
6867
link1 = links[0]
69-
assert span_link_trace_id_equals(link1["trace_id"], 2)
70-
assert link1["span_id"] == 987654321
71-
assert link1["attributes"] == {"reason": "terminated_context", "context_headers": "tracecontext"}
72-
assert link1["trace_id_high"] == 1229782938247303441
68+
assert link1.trace_id_low == 2
69+
assert link1.span_id == 987654321
70+
assert link1.attributes == {"reason": "terminated_context", "context_headers": "tracecontext"}
71+
assert link1.trace_id_high == 1229782938247303441
7372

7473
"""Datadog and tracecontext headers, trace-id does match, Datadog is primary
7574
context we want to make sure there's no span link since they match"""
@@ -90,7 +89,7 @@ def test_no_span_links_from_nonconflicting_contexts(self):
9089
trace = [
9190
span
9291
for _, _, span in interfaces.library.get_spans(self.req, full_trace=True)
93-
if _retrieve_span_links(span) is not None
92+
if len(span.get_span_links()) != 0
9493
and span["trace_id"] == 1
9594
and span["parent_id"] == 987654321 # Only fetch the trace that is related to the header extractions
9695
]
@@ -117,7 +116,7 @@ def test_no_span_links_from_invalid_trace_id(self):
117116
trace = [
118117
span
119118
for _, _, span in interfaces.library.get_spans(self.req, full_trace=True)
120-
if _retrieve_span_links(span) is not None
119+
if len(span.get_span_links()) != 0
121120
and span["trace_id"] == 5
122121
and span["parent_id"] == 987654324 # Only fetch the trace that is related to the header extractions
123122
]
@@ -147,7 +146,7 @@ def test_span_links_flags_from_conflicting_contexts(self):
147146
spans = [
148147
span
149148
for _, _, span in interfaces.library.get_spans(self.req, full_trace=True)
150-
if _retrieve_span_links(span) is not None
149+
if len(span.get_span_links()) != 0
151150
and span["trace_id"] == 2
152151
and span["parent_id"] == 987654321 # Only fetch the trace that is related to the header extractions
153152
]
@@ -156,10 +155,10 @@ def test_span_links_flags_from_conflicting_contexts(self):
156155
raise ValueError(f"Expected 1 span, got {len(spans)}")
157156

158157
span = spans[0]
159-
span_links = _retrieve_span_links(span)
158+
span_links = span.get_span_links()
160159
assert len(span_links) == 2
161160
link1 = span_links[0]
162-
assert link1["flags"] == 1 | TRACECONTEXT_FLAGS_SET
161+
assert link1.data["flags"] == 1 | DataDogSpanLink.TRACECONTEXT_FLAGS_SET
163162

164163

165164
@scenarios.default
@@ -183,7 +182,7 @@ def test_span_links_omit_tracestate_from_conflicting_contexts(self):
183182
spans = [
184183
span
185184
for _, _, span in interfaces.library.get_spans(self.req, full_trace=True)
186-
if _retrieve_span_links(span) is not None
185+
if len(span.get_span_links()) != 0
187186
and span["trace_id"] == 2
188187
and span["parent_id"] == 987654321 # Only fetch the trace that is related to the header extractions
189188
]
@@ -192,60 +191,10 @@ def test_span_links_omit_tracestate_from_conflicting_contexts(self):
192191
raise ValueError(f"Expected 1 span, got {len(spans)}")
193192

194193
span = spans[0]
195-
links = _retrieve_span_links(span)
194+
links = span.get_span_links()
196195
assert len(links) == 1
197196
link1 = links[0]
198-
assert link1.get("tracestate") is None
199-
200-
201-
def _normalize_v1_span_link(link: dict) -> dict:
202-
"""Ensure v1 span link has trace_id_high when trace_id is 128-bit hex string."""
203-
link = dict(link)
204-
tid = link.get("trace_id")
205-
if isinstance(tid, str) and tid.startswith("0x") and len(tid) > 18 and "trace_id_high" not in link:
206-
# 128-bit: high 64 bits (first 16 hex chars after 0x)
207-
link["trace_id_high"] = int(tid[2:18], 16)
208-
return link
209-
210-
211-
def _retrieve_span_links(span: DataDogLibrarySpan):
212-
if span.trace.format == LibraryTraceFormat.v10:
213-
# v1.0: span_links can be at top level or in attributes
214-
links = span.raw_span.get("span_links") or span.raw_span.get("attributes", {}).get("_dd.span_links")
215-
if links:
216-
return [_normalize_v1_span_link(lnk) for lnk in links]
217-
return None
218-
219-
if span.get("span_links") is not None:
220-
return span["span_links"]
221-
222-
if span["meta"].get("_dd.span_links") is not None:
223-
# Convert span_links tags into msgpack v0.4 format
224-
json_links = json.loads(span["meta"].get("_dd.span_links"))
225-
links = []
226-
for json_link in json_links:
227-
link = {}
228-
link["trace_id"] = int(json_link["trace_id"][-16:], base=16)
229-
link["span_id"] = int(json_link["span_id"], base=16)
230-
if len(json_link["trace_id"]) > 16:
231-
link["trace_id_high"] = int(json_link["trace_id"][:16], base=16)
232-
if "attributes" in json_link:
233-
link["attributes"] = json_link.get("attributes")
234-
if "tracestate" in json_link:
235-
link["tracestate"] = json_link.get("tracestate")
236-
elif "trace_state" in json_link:
237-
link["tracestate"] = json_link.get("trace_state")
238-
if "flags" in json_link:
239-
link["flags"] = json_link.get("flags") | 1 << 31
240-
else:
241-
link["flags"] = 0
242-
links.append(link)
243-
return links
244-
return None
245-
246-
247-
# The Datadog specific tracecontext flags to mark flags are set
248-
TRACECONTEXT_FLAGS_SET = 1 << 31
197+
assert link1.data.get("tracestate") is None
249198

250199

251200
@scenarios.default

tests/test_library_conf.py

Lines changed: 15 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
# This product includes software developed at Datadog (https://www.datadoghq.com/).
33
# Copyright 2021 Datadog, Inc.
44
import pytest
5-
from utils import weblog, interfaces, scenarios, features, logger
6-
from utils.dd_types import DataDogAgentSpan, AgentTraceFormat
5+
from utils import weblog, interfaces, scenarios, features
6+
from utils.dd_types import DataDogAgentSpan
77
from utils._context.header_tag_vars import (
88
CONFIG_COLON_LEADING,
99
CONFIG_COLON_TRAILING,
@@ -426,79 +426,6 @@ def test_trace_header_tags(self):
426426
assert span_meta[RESPONSE_PREFIX + key.lower()] == response_headers[key]
427427

428428

429-
# The Datadog specific tracecontext flags to mark flags are set
430-
TRACECONTEXT_FLAGS_SET = 1 << 31
431-
432-
433-
class SpanLink:
434-
def __init__(self, data: dict, trace_id: str, trace_id_low: int, trace_id_high: int):
435-
self._data = data
436-
self.trace_id = trace_id
437-
self.trace_id_low = trace_id_low
438-
self.trace_id_high = trace_id_high
439-
440-
self.attributes: dict[str, str] | None = data.get("attributes")
441-
self.trace_state: str | None = data.get("tracestate", data.get("trace_state"))
442-
self.flags: int = (data["flags"] | TRACECONTEXT_FLAGS_SET) if "flags" in data else 0
443-
444-
if "span_id" in self._data: # span_id is a string on base 16
445-
self.span_id = int(data["span_id"], base=16)
446-
elif "spanID" in self._data: # spanID is a string on base 10
447-
self.span_id = int(data["spanID"])
448-
else:
449-
raise ValueError(f"No span id exists in span link: {data}")
450-
451-
@staticmethod
452-
def from_span_links(data: dict) -> "SpanLink":
453-
return SpanLink(
454-
data, trace_id=data["traceID"], trace_id_high=int(data["traceIDHigh"]), trace_id_low=int(data["traceID"])
455-
)
456-
457-
@staticmethod
458-
def from_efficient_trace_payload_format(data: dict) -> "SpanLink":
459-
trace_id = data["traceID"]
460-
461-
return SpanLink(
462-
data,
463-
trace_id=trace_id,
464-
trace_id_high=(int(trace_id, 16) >> 64) & 0xFFFFFFFFFFFFFFFF,
465-
trace_id_low=int(trace_id, 16) & 0xFFFFFFFFFFFFFFFF,
466-
)
467-
468-
@staticmethod
469-
def from_legacy_format(data: dict) -> "SpanLink":
470-
trace_id = data["trace_id"]
471-
472-
return SpanLink(
473-
data,
474-
trace_id=trace_id,
475-
trace_id_high=int(trace_id[:16], base=16),
476-
trace_id_low=int(trace_id[-16:], base=16),
477-
)
478-
479-
def __str__(self) -> str:
480-
return str(self._data)
481-
482-
def __repr__(self) -> str:
483-
return repr(self._data)
484-
485-
486-
def get_span_links(span: DataDogAgentSpan) -> list[SpanLink]:
487-
if span.get("spanLinks") is not None:
488-
logger.info("Span links are stored inside span.spanLinks")
489-
return [SpanLink.from_span_links(data) for data in span.get("spanLinks")]
490-
491-
if span.trace.format == AgentTraceFormat.efficient_trace_payload_format and span.get("links") is not None:
492-
logger.info("Span links are stored inside span.links and trace format is v1")
493-
return [SpanLink.from_efficient_trace_payload_format(data) for data in span.get("links")]
494-
495-
logger.info("Span links are stored inside span.meta['_dd.span_links'] and trace format is legacy")
496-
raw = span.meta.get("_dd.span_links", [])
497-
raw_deserilialized = json.loads(raw) if isinstance(raw, (str, bytes, bytearray)) else raw
498-
499-
return [SpanLink.from_legacy_format(data) for data in raw_deserilialized]
500-
501-
502429
@scenarios.default
503430
@features.context_propagation_extract_behavior
504431
class Test_ExtractBehavior_Default:
@@ -528,7 +455,7 @@ def test_single_tracecontext(self):
528455
span = spans[0]
529456
assert trace_id == 1
530457
assert span.get("parentID") == "1"
531-
span_links = get_span_links(span)
458+
span_links = span.get_span_links()
532459
assert len(span_links) == 0, "Expected span links to be absent"
533460

534461
# Test the next outbound span context
@@ -568,7 +495,7 @@ def test_multiple_tracecontexts(self):
568495
assert span.get("parentID") == "2"
569496

570497
# Test the extracted span links: One span link per conflicting trace context
571-
span_links = get_span_links(span)
498+
span_links = span.get_span_links()
572499
assert len(span_links) == 1, "Expected span links to be present"
573500

574501
link = span_links[0]
@@ -619,7 +546,7 @@ def test_single_tracecontext(self):
619546
# Test the extracted span links: One span link for the incoming (Datadog trace context).
620547
# In the case that span links are generated for conflicting trace contexts, those span links
621548
# are not included in the new trace context
622-
span_links = get_span_links(span)
549+
span_links = span.get_span_links()
623550
assert len(span_links) == 1, "Expected span links to be present"
624551

625552
# Assert the Datadog (restarted) span link
@@ -670,7 +597,7 @@ def test_multiple_tracecontexts(self):
670597
# Test the extracted span links: One span link for the incoming (Datadog trace context).
671598
# In the case that span links are generated for conflicting trace contexts, those span links
672599
# are not included in the new trace context
673-
span_links = get_span_links(span)
600+
span_links = span.get_span_links()
674601
assert len(span_links) == 1, "Expected span links to be present"
675602

676603
# Assert the Datadog (restarted) span link
@@ -720,7 +647,7 @@ def test_multiple_tracecontexts_with_overrides(self):
720647
# Test the extracted span links: One span link for the incoming (Datadog trace context).
721648
# In the case that span links are generated for conflicting trace contexts, those span links
722649
# are not included in the new trace context
723-
span_links = get_span_links(span)
650+
span_links = span.get_span_links()
724651
assert len(span_links) == 1, "Expected span links to be present"
725652

726653
# Assert the Datadog (restarted) span link
@@ -756,7 +683,7 @@ def _get_otel_span(self, spans: list) -> DataDogAgentSpan:
756683
return span
757684
# Fallback: return whichever span has span links
758685
for span in spans:
759-
if get_span_links(span):
686+
if span.get_span_links():
760687
return span
761688
return spans[0]
762689

@@ -783,7 +710,7 @@ def test_single_tracecontext(self):
783710
assert span.get("traceID") != "1"
784711
assert span.get("parentID") is None
785712

786-
span_links = get_span_links(span)
713+
span_links = span.get_span_links()
787714
assert len(span_links) == 1, "Expected span links to be present"
788715

789716
link = span_links[0]
@@ -822,7 +749,7 @@ def test_multiple_tracecontexts(self):
822749
assert span.get("traceID") != "8687463697196027922"
823750
assert span.get("parentID") is None
824751

825-
span_links = get_span_links(span)
752+
span_links = span.get_span_links()
826753
assert len(span_links) == 1, "Expected span links to be present"
827754

828755
link = span_links[0]
@@ -860,7 +787,7 @@ def test_multiple_tracecontexts_with_overrides(self):
860787
assert span.get("traceID") != "1"
861788
assert span.get("parentID") is None
862789

863-
span_links = get_span_links(span)
790+
span_links = span.get_span_links()
864791
assert len(span_links) == 1, "Expected span links to be present"
865792

866793
link = span_links[0]
@@ -904,7 +831,7 @@ def test_single_tracecontext(self):
904831
span = spans[0]
905832
assert span.get("traceID") != "1"
906833
assert span.get("parentID") is None
907-
span_links = get_span_links(span)
834+
span_links = span.get_span_links()
908835
assert len(span_links) == 0, "Expected span links to be absent"
909836

910837
# Test the next outbound span context
@@ -944,7 +871,7 @@ def test_multiple_tracecontexts(self):
944871
span.get("traceID") != "8687463697196027922" # Lower 64-bits of traceparent
945872
)
946873
assert span.get("parentID") is None
947-
span_links = get_span_links(span)
874+
span_links = span.get_span_links()
948875
assert len(span_links) == 0, "Expected span links to be absent"
949876

950877
# Test the next outbound span context
@@ -988,7 +915,7 @@ def test_single_tracecontext(self):
988915
# Test the extracted span links: One span link for the incoming (Datadog trace context).
989916
# In the case that span links are generated for conflicting trace contexts, those span links
990917
# are not included in the new trace context
991-
span_links = get_span_links(span)
918+
span_links = span.get_span_links()
992919
assert len(span_links) == 1, "Expected span links to be present"
993920

994921
# Assert the Datadog (restarted) span link
@@ -1039,7 +966,7 @@ def test_multiple_tracecontexts(self):
1039966
# Test the extracted span links: One span link for the incoming (Datadog trace context).
1040967
# In the case that span links are generated for conflicting trace contexts, those span links
1041968
# are not included in the new trace context
1042-
span_links = get_span_links(span)
969+
span_links = span.get_span_links()
1043970
assert len(span_links) == 1, "Expected span links to be present"
1044971

1045972
# Assert the Datadog (restarted) span link

tests/test_v1_payloads.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,20 @@ def setup_span_links_present(self):
6767
def test_span_links_present(self):
6868
"""V1 spans carrying span links expose them at the top level or in attributes"""
6969
spans_with_links = [
70-
span
71-
for _, _, span in interfaces.library.get_spans(self.r, full_trace=True)
72-
if span.raw_span.get("span_links") or span.raw_span.get("attributes", {}).get("_dd.span_links")
70+
span for _, _, span in interfaces.library.get_spans(self.r, full_trace=True) if span.get_span_links()
7371
]
7472
assert len(spans_with_links) >= 1, "Expected at least one span with span links"
7573

7674
link_carrier = spans_with_links[0]
77-
links = link_carrier.raw_span.get("span_links") or []
75+
assert link_carrier.trace.format == LibraryTraceFormat.v10
76+
links = link_carrier.get_span_links()
7877

7978
assert len(links) >= 1
8079
link = links[0]
8180

82-
assert isinstance(link["trace_id"], str)
83-
assert link["trace_id"].startswith("0x")
84-
assert isinstance(link["span_id"], int)
81+
assert isinstance(link.data["trace_id"], str)
82+
assert link.data["trace_id"].startswith("0x")
83+
assert isinstance(link.data["span_id"], int)
8584

8685

8786
@scenarios.apm_tracing_efficient_payload

utils/dd_types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ._datadog_agent_trace import DataDogAgentSpan, DataDogAgentTrace, AgentTraceFormat
22
from ._datadog_library_trace import DataDogLibraryTrace, DataDogLibrarySpan, LibraryTraceFormat
3+
from ._datadog_span_link import DataDogSpanLink
34
from ._utils import is_same_boolean
45

56
__all__ = [
@@ -8,6 +9,7 @@
89
"DataDogAgentTrace",
910
"DataDogLibrarySpan",
1011
"DataDogLibraryTrace",
12+
"DataDogSpanLink",
1113
"LibraryTraceFormat",
1214
"is_same_boolean",
1315
]

0 commit comments

Comments
 (0)