Skip to content

Commit 9fdc902

Browse files
authored
Span link support (#7325)
1 parent 26fc422 commit 9fdc902

1 file changed

Lines changed: 125 additions & 127 deletions

File tree

tests/test_library_conf.py

Lines changed: 125 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
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
5+
from utils import weblog, interfaces, scenarios, features, logger
66
from utils.dd_types import DataDogAgentSpan, AgentTraceFormat
77
from utils._context.header_tag_vars import (
88
CONFIG_COLON_LEADING,
@@ -430,45 +430,73 @@ def test_trace_header_tags(self):
430430
TRACECONTEXT_FLAGS_SET = 1 << 31
431431

432432

433-
def retrieve_span_links(span: DataDogAgentSpan) -> list[dict] | None:
434-
"""Retrieves span links from a span.
435-
Returns the format of the span links as it may differ from the trace format emitted by the agent
436-
"""
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]:
437487
if span.get("spanLinks") is not None:
438-
return span["spanLinks"]
488+
logger.info("Span links are stored inside span.spanLinks")
489+
return [SpanLink.from_span_links(data) for data in span.get("spanLinks")]
439490

440491
if span.trace.format == AgentTraceFormat.efficient_trace_payload_format and span.get("links") is not None:
441-
return span["links"]
442-
443-
span_meta = span.meta
444-
445-
if span_meta.get("_dd.span_links") is None:
446-
return None
447-
448-
# Convert span_links tags into msgpack v0.4 format. The v1 trace proxy
449-
# normalizes JSON meta values while v0.4 payloads retain the serialized
450-
# string, so accept both representations here.
451-
raw_links = span_meta["_dd.span_links"]
452-
json_links = json.loads(raw_links) if isinstance(raw_links, (str, bytes, bytearray)) else raw_links
453-
links = []
454-
for json_link in json_links:
455-
link = {}
456-
link["traceID"] = int(json_link["trace_id"][-16:], base=16)
457-
link["spanID"] = int(json_link["span_id"], base=16)
458-
if len(json_link["trace_id"]) > 16:
459-
link["traceIDHigh"] = int(json_link["trace_id"][:16], base=16)
460-
if "attributes" in json_link:
461-
link["attributes"] = json_link.get("attributes")
462-
if "tracestate" in json_link:
463-
link["tracestate"] = json_link.get("tracestate")
464-
elif "trace_state" in json_link:
465-
link["tracestate"] = json_link.get("trace_state")
466-
if "flags" in json_link:
467-
link["flags"] = json_link.get("flags") | TRACECONTEXT_FLAGS_SET
468-
else:
469-
link["flags"] = 0
470-
links.append(link)
471-
return links
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]
472500

473501

474502
@scenarios.default
@@ -500,8 +528,8 @@ def test_single_tracecontext(self):
500528
span = spans[0]
501529
assert trace_id == 1
502530
assert span.get("parentID") == "1"
503-
span_links = retrieve_span_links(span)
504-
assert span_links is None
531+
span_links = get_span_links(span)
532+
assert len(span_links) == 0, "Expected span links to be absent"
505533

506534
# Test the next outbound span context
507535
assert self.r.status_code == 200
@@ -540,17 +568,15 @@ def test_multiple_tracecontexts(self):
540568
assert span.get("parentID") == "2"
541569

542570
# Test the extracted span links: One span link per conflicting trace context
543-
span_links = retrieve_span_links(span)
544-
assert span_links is not None, "Expected span links to be present"
545-
assert len(span_links) == 1
571+
span_links = get_span_links(span)
572+
assert len(span_links) == 1, "Expected span links to be present"
546573

547574
link = span_links[0]
548-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
549575
# Assert the W3C Trace Context (conflicting trace context) span link according to the format
550-
assert trace_id_low == 8687463697196027922 # int(0x7890123456789012)
551-
assert trace_id_high == 1311768467284833366 # int(0x1234567890123456)
552-
assert int(link["spanID"]) == 1311768467284833366 # int (0x1234567890123456)
553-
assert link["attributes"] == {"reason": "terminated_context", "context_headers": "tracecontext"}
576+
assert link.trace_id_low == 8687463697196027922, link # int(0x7890123456789012)
577+
assert link.trace_id_high == 1311768467284833366, link # int(0x1234567890123456)
578+
assert link.span_id == 1311768467284833366, link # int (0x1234567890123456)
579+
assert link.attributes == {"reason": "terminated_context", "context_headers": "tracecontext"}
554580

555581
# Test the next outbound span context
556582
assert self.r.status_code == 200
@@ -593,17 +619,15 @@ def test_single_tracecontext(self):
593619
# Test the extracted span links: One span link for the incoming (Datadog trace context).
594620
# In the case that span links are generated for conflicting trace contexts, those span links
595621
# are not included in the new trace context
596-
span_links = retrieve_span_links(span)
597-
assert span_links is not None, "Expected span links to be present"
598-
assert len(span_links) == 1
622+
span_links = get_span_links(span)
623+
assert len(span_links) == 1, "Expected span links to be present"
599624

600625
# Assert the Datadog (restarted) span link
601626
link = span_links[0]
602-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
603-
assert trace_id_low == 1
604-
assert trace_id_high == 1229782938247303441
605-
assert int(link["spanID"]) == 1
606-
assert link["attributes"] == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
627+
assert link.trace_id_low == 1
628+
assert link.trace_id_high == 1229782938247303441
629+
assert link.span_id == 1
630+
assert link.attributes == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
607631

608632
# Test the next outbound span context
609633
assert self.r.status_code == 200
@@ -646,17 +670,15 @@ def test_multiple_tracecontexts(self):
646670
# Test the extracted span links: One span link for the incoming (Datadog trace context).
647671
# In the case that span links are generated for conflicting trace contexts, those span links
648672
# are not included in the new trace context
649-
span_links = retrieve_span_links(span)
650-
assert span_links is not None, "Expected span links to be present"
651-
assert len(span_links) == 1
673+
span_links = get_span_links(span)
674+
assert len(span_links) == 1, "Expected span links to be present"
652675

653676
# Assert the Datadog (restarted) span link
654677
link = span_links[0]
655-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
656-
assert trace_id_low == 1
657-
assert trace_id_high == 1229782938247303441
658-
assert int(link["spanID"]) == 1
659-
assert link["attributes"] == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
678+
assert link.trace_id_low == 1
679+
assert link.trace_id_high == 1229782938247303441
680+
assert link.span_id == 1
681+
assert link.attributes == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
660682

661683
# Test the next outbound span context
662684
assert self.r.status_code == 200
@@ -698,18 +720,15 @@ def test_multiple_tracecontexts_with_overrides(self):
698720
# Test the extracted span links: One span link for the incoming (Datadog trace context).
699721
# In the case that span links are generated for conflicting trace contexts, those span links
700722
# are not included in the new trace context
701-
span_links = retrieve_span_links(span)
702-
assert span_links is not None, "Expected span links to be present"
703-
assert len(span_links) == 1
723+
span_links = get_span_links(span)
724+
assert len(span_links) == 1, "Expected span links to be present"
704725

705726
# Assert the Datadog (restarted) span link
706727
link = span_links[0]
707-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
708-
709-
assert trace_id_low == 1
710-
assert trace_id_high == 1229782938247303441
711-
assert int(link["spanID"]) == 1311768467284833366
712-
assert link["attributes"] == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
728+
assert link.trace_id_low == 1
729+
assert link.trace_id_high == 1229782938247303441
730+
assert link.span_id == 1311768467284833366
731+
assert link.attributes == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
713732

714733
# Test the next outbound span context
715734
assert self.r.status_code == 200
@@ -737,7 +756,7 @@ def _get_otel_span(self, spans: list) -> DataDogAgentSpan:
737756
return span
738757
# Fallback: return whichever span has span links
739758
for span in spans:
740-
if retrieve_span_links(span) is not None:
759+
if get_span_links(span):
741760
return span
742761
return spans[0]
743762

@@ -764,16 +783,14 @@ def test_single_tracecontext(self):
764783
assert span.get("traceID") != "1"
765784
assert span.get("parentID") is None
766785

767-
span_links = retrieve_span_links(span)
768-
assert span_links is not None, "Expected span links to be present"
769-
assert len(span_links) == 1
786+
span_links = get_span_links(span)
787+
assert len(span_links) == 1, "Expected span links to be present"
770788

771789
link = span_links[0]
772-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
773-
assert trace_id_low == 1
774-
assert trace_id_high == 1229782938247303441
775-
assert int(link["spanID"]) == 1
776-
assert link["attributes"] == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
790+
assert link.trace_id_low == 1
791+
assert link.trace_id_high == 1229782938247303441
792+
assert link.span_id == 1
793+
assert link.attributes == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
777794

778795
assert self.r.status_code == 200
779796
data = json.loads(self.r.text)
@@ -805,16 +822,14 @@ def test_multiple_tracecontexts(self):
805822
assert span.get("traceID") != "8687463697196027922"
806823
assert span.get("parentID") is None
807824

808-
span_links = retrieve_span_links(span)
809-
assert span_links is not None, "Expected span links to be present"
810-
assert len(span_links) == 1
825+
span_links = get_span_links(span)
826+
assert len(span_links) == 1, "Expected span links to be present"
811827

812828
link = span_links[0]
813-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
814-
assert trace_id_low == 1
815-
assert trace_id_high == 1229782938247303441
816-
assert int(link["spanID"]) == 1
817-
assert link["attributes"] == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
829+
assert link.trace_id_low == 1
830+
assert link.trace_id_high == 1229782938247303441
831+
assert link.span_id == 1
832+
assert link.attributes == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
818833

819834
assert self.r.status_code == 200
820835
data = json.loads(self.r.text)
@@ -845,16 +860,14 @@ def test_multiple_tracecontexts_with_overrides(self):
845860
assert span.get("traceID") != "1"
846861
assert span.get("parentID") is None
847862

848-
span_links = retrieve_span_links(span)
849-
assert span_links is not None, "Expected span links to be present"
850-
assert len(span_links) == 1
863+
span_links = get_span_links(span)
864+
assert len(span_links) == 1, "Expected span links to be present"
851865

852866
link = span_links[0]
853-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
854-
assert trace_id_low == 1
855-
assert trace_id_high == 1229782938247303441
856-
assert int(link["spanID"]) == 1311768467284833366
857-
assert link["attributes"] == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
867+
assert link.trace_id_low == 1
868+
assert link.trace_id_high == 1229782938247303441
869+
assert link.span_id == 1311768467284833366
870+
assert link.attributes == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
858871

859872
assert self.r.status_code == 200
860873
data = json.loads(self.r.text)
@@ -864,17 +877,6 @@ def test_multiple_tracecontexts_with_overrides(self):
864877
assert "key1=value1" in data["request_headers"]["baggage"]
865878

866879

867-
def _get_span_link_trace_id(link: dict, span_format: AgentTraceFormat) -> tuple[int, int]:
868-
"""Returns the trace ID of a span link according to its format split into high and low 64 bits"""
869-
if span_format == AgentTraceFormat.efficient_trace_payload_format:
870-
trace_id_low = int(link["traceID"], 16) & 0xFFFFFFFFFFFFFFFF
871-
trace_id_high = (int(link["traceID"], 16) >> 64) & 0xFFFFFFFFFFFFFFFF
872-
else:
873-
trace_id_low = int(link["traceID"])
874-
trace_id_high = int(link["traceIDHigh"])
875-
return trace_id_high, trace_id_low
876-
877-
878880
@scenarios.tracing_config_nondefault_2
879881
@features.context_propagation_extract_behavior
880882
class Test_ExtractBehavior_Ignore:
@@ -902,8 +904,8 @@ def test_single_tracecontext(self):
902904
span = spans[0]
903905
assert span.get("traceID") != "1"
904906
assert span.get("parentID") is None
905-
span_links = retrieve_span_links(span)
906-
assert span_links is None
907+
span_links = get_span_links(span)
908+
assert len(span_links) == 0, "Expected span links to be absent"
907909

908910
# Test the next outbound span context
909911
assert self.r.status_code == 200
@@ -942,8 +944,8 @@ def test_multiple_tracecontexts(self):
942944
span.get("traceID") != "8687463697196027922" # Lower 64-bits of traceparent
943945
)
944946
assert span.get("parentID") is None
945-
span_links = retrieve_span_links(span)
946-
assert span_links is None
947+
span_links = get_span_links(span)
948+
assert len(span_links) == 0, "Expected span links to be absent"
947949

948950
# Test the next outbound span context
949951
assert self.r.status_code == 200
@@ -986,17 +988,15 @@ def test_single_tracecontext(self):
986988
# Test the extracted span links: One span link for the incoming (Datadog trace context).
987989
# In the case that span links are generated for conflicting trace contexts, those span links
988990
# are not included in the new trace context
989-
span_links = retrieve_span_links(span)
990-
assert span_links is not None, "Expected span links to be present"
991-
assert len(span_links) == 1
991+
span_links = get_span_links(span)
992+
assert len(span_links) == 1, "Expected span links to be present"
992993

993994
# Assert the Datadog (restarted) span link
994995
link = span_links[0]
995-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
996-
assert trace_id_low == 1
997-
assert int(link["spanID"]) == 1
998-
assert trace_id_high == 1229782938247303441
999-
assert link["attributes"] == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
996+
assert link.trace_id_low == 1
997+
assert link.trace_id_high == 1229782938247303441, f"link: {link}"
998+
assert link.span_id == 1
999+
assert link.attributes == {"reason": "propagation_behavior_extract", "context_headers": "datadog"}
10001000

10011001
# Test the next outbound span context
10021002
assert self.r.status_code == 200
@@ -1039,16 +1039,14 @@ def test_multiple_tracecontexts(self):
10391039
# Test the extracted span links: One span link for the incoming (Datadog trace context).
10401040
# In the case that span links are generated for conflicting trace contexts, those span links
10411041
# are not included in the new trace context
1042-
span_links = retrieve_span_links(span)
1043-
assert span_links is not None, "Expected span links to be present"
1044-
assert len(span_links) == 1
1042+
span_links = get_span_links(span)
1043+
assert len(span_links) == 1, "Expected span links to be present"
10451044

10461045
# Assert the Datadog (restarted) span link
10471046
link = span_links[0]
1048-
trace_id_high, trace_id_low = _get_span_link_trace_id(link, span.trace.format)
1049-
assert trace_id_low == 1
1050-
assert trace_id_high == 1229782938247303441
1051-
assert int(link["spanID"]) == 1
1047+
assert link.trace_id_low == 1
1048+
assert link.trace_id_high == 1229782938247303441
1049+
assert link.span_id == 1
10521050

10531051
# Test the next outbound span context
10541052
assert self.r.status_code == 200

0 commit comments

Comments
 (0)