22# This product includes software developed at Datadog (https://www.datadoghq.com/).
33# Copyright 2021 Datadog, Inc.
44import pytest
5- from utils import weblog , interfaces , scenarios , features
5+ from utils import weblog , interfaces , scenarios , features , logger
66from utils .dd_types import DataDogAgentSpan , AgentTraceFormat
77from utils ._context .header_tag_vars import (
88 CONFIG_COLON_LEADING ,
@@ -430,45 +430,73 @@ def test_trace_header_tags(self):
430430TRACECONTEXT_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
880882class 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