2
2
import os
3
3
import threading
4
4
from typing import TYPE_CHECKING # noqa:F401
5
+ from typing import Any # noqa:F401
6
+ from typing import Dict # noqa:F401
7
+ from typing import List # noqa:F401
8
+ from typing import Optional # noqa:F401
9
+ from typing import Tuple # noqa:F401
5
10
from uuid import uuid4
6
11
7
12
from ddtrace .ext import SpanTypes
28
33
log = get_logger (__name__ )
29
34
30
35
if TYPE_CHECKING : # pragma: no cover
31
- from typing import Any # noqa:F401
32
- from typing import Dict # noqa:F401
33
- from typing import List # noqa:F401
34
- from typing import Optional # noqa:F401
35
-
36
36
from ddtrace ._trace .span import Span # noqa:F401
37
37
38
38
@@ -42,13 +42,15 @@ class CIVisibilityEncoderV01(BufferedEncoder):
42
42
TEST_SUITE_EVENT_VERSION = 1
43
43
TEST_EVENT_VERSION = 2
44
44
ENDPOINT_TYPE = ENDPOINT .TEST_CYCLE
45
+ _MAX_PAYLOAD_SIZE = 5 * 1024 * 1024 # 5MB
45
46
46
47
def __init__ (self , * args ):
47
48
# DEV: args are not used here, but are used by BufferedEncoder's __cinit__() method,
48
49
# which is called implicitly by Cython.
49
50
super (CIVisibilityEncoderV01 , self ).__init__ ()
51
+ self ._metadata = {} # type: Dict[str, Dict[str, str]]
50
52
self ._lock = threading .RLock ()
51
- self ._metadata = {}
53
+ self ._is_not_xdist_worker = os . getenv ( "PYTEST_XDIST_WORKER" ) is None
52
54
self ._init_buffer ()
53
55
54
56
def __len__ (self ):
@@ -68,18 +70,21 @@ def put(self, spans):
68
70
self .buffer .append (spans )
69
71
70
72
def encode_traces (self , traces ):
71
- return self ._build_payload (traces = traces )
73
+ return self ._build_payload (traces = traces )[ 0 ]
72
74
73
- def encode (self ):
75
+ def encode (self ) -> List [ Tuple [ Optional [ bytes ], int ]] :
74
76
with self ._lock :
75
- with StopWatch () as sw :
76
- payload = self ._build_payload (self .buffer )
77
- record_endpoint_payload_events_serialization_time (endpoint = self .ENDPOINT_TYPE , seconds = sw .elapsed ())
78
- buffer_size = len (self .buffer )
79
- if not buffer_size :
77
+ if not self .buffer :
80
78
return []
81
- self ._init_buffer ()
82
- return [(payload , buffer_size )]
79
+ payloads = []
80
+ while self .buffer :
81
+ with StopWatch () as sw :
82
+ payload , count = self ._build_payload (self .buffer )
83
+ payloads .append ((payload , count ))
84
+ record_endpoint_payload_events_serialization_time (endpoint = self .ENDPOINT_TYPE , seconds = sw .elapsed ())
85
+ if count :
86
+ self .buffer = self .buffer [count :]
87
+ return payloads
83
88
84
89
def _get_parent_session (self , traces ):
85
90
for trace in traces :
@@ -89,29 +94,65 @@ def _get_parent_session(self, traces):
89
94
return 0
90
95
91
96
def _build_payload (self , traces ):
92
- new_parent_session_span_id = self ._get_parent_session (traces )
93
- is_not_xdist_worker = os .getenv ("PYTEST_XDIST_WORKER" ) is None
94
- normalized_spans = [
95
- self ._convert_span (span , trace [0 ].context .dd_origin , new_parent_session_span_id )
96
- for trace in traces
97
- for span in trace
98
- if (is_not_xdist_worker or span .get_tag (EVENT_TYPE ) != SESSION_TYPE )
99
- ]
100
- if not normalized_spans :
101
- return None
102
- record_endpoint_payload_events_count (endpoint = ENDPOINT .TEST_CYCLE , count = len (normalized_spans ))
97
+ # type: (List[List[Span]]) -> Tuple[Optional[bytes], int]
98
+ if not traces :
99
+ return []
103
100
104
- # TODO: Split the events in several payloads as needed to avoid hitting the intake's maximum payload size.
101
+ new_parent_session_span_id = self ._get_parent_session (traces )
102
+ return self ._send_all_or_half_spans (traces , new_parent_session_span_id )
103
+
104
+ def _send_all_or_half_spans (self , traces , new_parent_session_span_id ):
105
+ # Convert all traces to spans with filtering
106
+ all_spans_with_trace_info = self ._convert_traces_to_spans (traces , new_parent_session_span_id )
107
+ total_traces = len (traces )
108
+
109
+ # Get all spans (flattened)
110
+ all_spans = [span for _ , trace_spans in all_spans_with_trace_info for span in trace_spans ]
111
+
112
+ if not all_spans :
113
+ log .debug ("No spans to encode after filtering, returning empty payload" )
114
+ return None , total_traces
115
+
116
+ # Try to fit all spans first (optimistic case)
117
+ payload = self ._create_payload_from_spans (all_spans )
118
+ if len (payload ) <= self ._MAX_PAYLOAD_SIZE or total_traces <= 1 :
119
+ record_endpoint_payload_events_count (endpoint = ENDPOINT .TEST_CYCLE , count = len (all_spans ))
120
+ return payload , total_traces
121
+
122
+ mid = (total_traces + 1 ) // 2
123
+ return self ._send_all_or_half_spans (traces [:mid ], new_parent_session_span_id )
124
+
125
+ def _convert_traces_to_spans (self , traces , new_parent_session_span_id ):
126
+ # type: (List[List[Span]], Optional[int]) -> List[Tuple[int, List[Dict[str, Any]]]]
127
+ """Convert all traces to spans with xdist filtering applied."""
128
+ all_spans_with_trace_info = []
129
+ for trace_idx , trace in enumerate (traces ):
130
+ trace_spans = [
131
+ self ._convert_span (span , trace [0 ].context .dd_origin , new_parent_session_span_id )
132
+ for span in trace
133
+ if self ._is_not_xdist_worker or span .get_tag (EVENT_TYPE ) != SESSION_TYPE
134
+ ]
135
+ all_spans_with_trace_info .append ((trace_idx , trace_spans ))
136
+
137
+ return all_spans_with_trace_info
138
+
139
+ def _create_payload_from_spans (self , spans ):
140
+ # type: (List[Dict[str, Any]]) -> bytes
141
+ """Create a payload from the given spans."""
105
142
return CIVisibilityEncoderV01 ._pack_payload (
106
- {"version" : self .PAYLOAD_FORMAT_VERSION , "metadata" : self ._metadata , "events" : normalized_spans }
143
+ {
144
+ "version" : self .PAYLOAD_FORMAT_VERSION ,
145
+ "metadata" : self ._metadata ,
146
+ "events" : spans ,
147
+ }
107
148
)
108
149
109
150
@staticmethod
110
151
def _pack_payload (payload ):
111
152
return msgpack_packb (payload )
112
153
113
- def _convert_span (self , span , dd_origin , new_parent_session_span_id = 0 ):
114
- # type: (Span, str, Optional[int]) -> Dict[str, Any]
154
+ def _convert_span (self , span , dd_origin = None , new_parent_session_span_id = 0 ):
155
+ # type: (Span, Optional[ str] , Optional[int]) -> Dict[str, Any]
115
156
sp = JSONEncoderV2 ._span_to_dict (span )
116
157
sp = JSONEncoderV2 ._normalize_span (sp )
117
158
sp ["type" ] = span .get_tag (EVENT_TYPE ) or span .span_type
@@ -220,7 +261,7 @@ def _build_body(self, data):
220
261
def _build_data (self , traces ):
221
262
# type: (List[List[Span]]) -> Optional[bytes]
222
263
normalized_covs = [
223
- self ._convert_span (span , "" )
264
+ self ._convert_span (span )
224
265
for trace in traces
225
266
for span in trace
226
267
if (COVERAGE_TAG_NAME in span .get_tags () or span .get_struct_tag (COVERAGE_TAG_NAME ) is not None )
@@ -232,14 +273,14 @@ def _build_data(self, traces):
232
273
return msgpack_packb ({"version" : self .PAYLOAD_FORMAT_VERSION , "coverages" : normalized_covs })
233
274
234
275
def _build_payload (self , traces ):
235
- # type: (List[List[Span]]) -> Optional[bytes]
276
+ # type: (List[List[Span]]) -> Tuple[ Optional[bytes], int ]
236
277
data = self ._build_data (traces )
237
278
if not data :
238
- return None
239
- return b"\r \n " .join (self ._build_body (data ))
279
+ return None , 0
280
+ return b"\r \n " .join (self ._build_body (data )), len ( data )
240
281
241
- def _convert_span (self , span , dd_origin , new_parent_session_span_id = 0 ):
242
- # type: (Span, str, Optional[int]) -> Dict[str, Any]
282
+ def _convert_span (self , span , dd_origin = None , new_parent_session_span_id = 0 ):
283
+ # type: (Span, Optional[ str] , Optional[int]) -> Dict[str, Any]
243
284
# DEV: new_parent_session_span_id is unused here, but it is used in super class
244
285
files : Dict [str , Any ] = {}
245
286
0 commit comments