1
1
from collections import defaultdict
2
2
from collections .abc import Mapping
3
3
from datetime import datetime , timedelta
4
- from typing import Any , TypedDict , cast
4
+ from typing import Any , Literal , TypedDict , cast
5
5
6
+ import sentry_sdk
6
7
from rest_framework import serializers
7
8
from rest_framework .request import Request
8
9
from rest_framework .response import Response
10
+ from snuba_sdk import Column , Condition , Op
9
11
10
12
from sentry import options
11
13
from sentry .api .api_owners import ApiOwner
19
21
from sentry .search .events .types import ParamsType , QueryBuilderConfig
20
22
from sentry .snuba .dataset import Dataset
21
23
from sentry .snuba .referrer import Referrer
24
+ from sentry .utils .snuba import bulk_snql_query
25
+
26
+
27
+ class TraceInterval (TypedDict ):
28
+ project : str | None
29
+ start : int
30
+ end : int
31
+ kind : Literal ["project" , "missing" , "unknown" ]
22
32
23
33
24
34
class TraceResult (TypedDict ):
25
35
trace : str
26
36
numSpans : int
27
37
name : str | None
28
38
duration : int
39
+ start : int
40
+ end : int
41
+ breakdowns : list [TraceInterval ]
29
42
spans : list [Mapping [str , Any ]]
30
43
31
44
@@ -118,34 +131,90 @@ def data_fn(offset: int, limit: int):
118
131
snuba_params .start = min_timestamp - buffer
119
132
snuba_params .end = max_timestamp + buffer
120
133
134
+ trace_condition = f"trace:[{ ', ' .join (spans_by_trace .keys ())} ]"
135
+
121
136
with handle_query_errors ():
122
- builder = SpansIndexedQueryBuilder (
137
+ breakdowns_query = SpansIndexedQueryBuilder (
138
+ Dataset .SpansIndexed ,
139
+ cast (ParamsType , params ),
140
+ snuba_params = snuba_params ,
141
+ query = f"{ trace_condition } " ,
142
+ selected_columns = [
143
+ "trace" ,
144
+ "project" ,
145
+ "transaction" ,
146
+ "first_seen()" ,
147
+ "last_seen()" ,
148
+ ],
149
+ orderby = ["first_seen()" , "last_seen()" ],
150
+ # limit the number of segments we fetch per trace so a single
151
+ # large trace does not result in the rest being blank
152
+ limitby = ("trace" , int (10_000 / len (spans_by_trace ))),
153
+ config = QueryBuilderConfig (
154
+ functions_acl = ["trace_name" , "first_seen" , "last_seen" ],
155
+ transform_alias_to_input_format = True ,
156
+ ),
157
+ )
158
+ # TODO: this should be `is_transaction:1` but there's some
159
+ # boolean mapping that's not working for this field
160
+ breakdowns_query .add_conditions (
161
+ [
162
+ Condition (Column ("is_segment" ), Op .EQ , 1 ),
163
+ ]
164
+ )
165
+
166
+ with handle_query_errors ():
167
+ traces_meta_query = SpansIndexedQueryBuilder (
123
168
Dataset .SpansIndexed ,
124
169
cast (ParamsType , params ),
125
170
snuba_params = snuba_params ,
126
- query = f"trace:[{ ', ' .join (spans_by_trace .keys ())} ]" ,
127
- selected_columns = ["trace" , "count()" , "trace_name()" , "elapsed()" ],
171
+ query = trace_condition ,
172
+ selected_columns = [
173
+ "trace" ,
174
+ "count()" ,
175
+ "trace_name()" ,
176
+ "first_seen()" ,
177
+ "last_seen()" ,
178
+ ],
128
179
limit = len (spans_by_trace ),
129
180
config = QueryBuilderConfig (
130
- functions_acl = ["trace_name" , "elapsed " ],
181
+ functions_acl = ["trace_name" , "first_seen" , "last_seen " ],
131
182
transform_alias_to_input_format = True ,
132
183
),
133
184
)
134
- trace_results = builder .run_query (Referrer .API_TRACE_EXPLORER_TRACES_META .value )
135
- trace_results = builder .process_results (trace_results )
185
+
186
+ with handle_query_errors ():
187
+ results = bulk_snql_query (
188
+ [
189
+ breakdowns_query .get_snql_query (),
190
+ traces_meta_query .get_snql_query (),
191
+ ],
192
+ Referrer .API_TRACE_EXPLORER_TRACES_META .value ,
193
+ )
194
+ breakdowns_results = breakdowns_query .process_results (results [0 ])
195
+ traces_meta_results = traces_meta_query .process_results (results [1 ])
196
+
197
+ try :
198
+ breakdowns = process_breakdowns (breakdowns_results ["data" ])
199
+ except Exception as e :
200
+ sentry_sdk .capture_exception (e )
201
+ breakdowns = defaultdict (list )
136
202
137
203
traces : list [TraceResult ] = [
138
204
{
139
205
"trace" : row ["trace" ],
140
206
"numSpans" : row ["count()" ],
141
207
"name" : row ["trace_name()" ],
142
- "duration" : row ["elapsed()" ],
208
+ "duration" : row ["last_seen()" ] - row ["first_seen()" ],
209
+ "start" : row ["first_seen()" ],
210
+ "end" : row ["last_seen()" ],
211
+ "breakdowns" : breakdowns [row ["trace" ]],
143
212
"spans" : [
144
213
{field : span [field ] for field in serialized ["field" ]}
145
214
for span in spans_by_trace [row ["trace" ]]
146
215
],
147
216
}
148
- for row in trace_results ["data" ]
217
+ for row in traces_meta_results ["data" ]
149
218
]
150
219
151
220
return {"data" : traces , "meta" : meta }
@@ -162,3 +231,105 @@ def data_fn(offset: int, limit: int):
162
231
dataset = Dataset .SpansIndexed ,
163
232
),
164
233
)
234
+
235
+
236
+ def process_breakdowns (data ):
237
+ trace_stacks : Mapping [str , list [TraceInterval ]] = defaultdict (list )
238
+ breakdowns_by_trace : Mapping [str , list [TraceInterval ]] = defaultdict (list )
239
+
240
+ def breakdown_append (breakdown , interval ):
241
+ breakdown .append (interval )
242
+
243
+ def stack_pop (stack ):
244
+ interval = stack .pop ()
245
+ if stack :
246
+ # when popping an interval off the stack, it means that we've
247
+ # consumed up to this point in time, so we update the transaction
248
+ # above it to the remaining interval
249
+ stack [- 1 ]["start" ] = interval ["end" ]
250
+ return interval
251
+
252
+ def stack_clear (breakdown , stack , until = None ):
253
+ interval = None
254
+ while stack :
255
+ if until is not None and stack [- 1 ]["end" ] > until :
256
+ break
257
+
258
+ item = stack_pop (stack )
259
+
260
+ # While popping the stack, we adjust the start
261
+ # of the intervals in the stack, so it's possible
262
+ # we produce an invalid interval. Make sure to
263
+ # filter them out here
264
+ if item ["start" ] <= item ["end" ]:
265
+ interval = item
266
+ breakdown_append (breakdown , interval )
267
+ return interval
268
+
269
+ for row in data :
270
+ stack = trace_stacks [row ["trace" ]]
271
+ cur : TraceInterval = {
272
+ "project" : row ["project" ],
273
+ "start" : row ["first_seen()" ],
274
+ "end" : row ["last_seen()" ],
275
+ "kind" : "project" ,
276
+ }
277
+
278
+ # nothing on the stack yet, so directly push onto
279
+ # stack and wait for next item to come so an
280
+ # interval can be determined
281
+ if not stack :
282
+ stack .append (cur )
283
+ continue
284
+
285
+ breakdown = breakdowns_by_trace [row ["trace" ]]
286
+ prev = stack [- 1 ]
287
+ if prev ["end" ] <= cur ["start" ]:
288
+ # This implies there's no overlap between this transaction
289
+ # and the previous transaction. So we're done with the whole stack
290
+
291
+ last = stack_clear (breakdown , stack , until = cur ["start" ])
292
+
293
+ # if there's a gap between prev end and cur start
294
+ if not stack or stack [- 1 ]["end" ] < cur ["start" ]:
295
+ if last is not None and last ["end" ] < cur ["start" ]:
296
+ breakdown_append (
297
+ breakdown ,
298
+ {
299
+ "project" : None ,
300
+ "start" : prev ["end" ],
301
+ "end" : cur ["start" ],
302
+ "kind" : "missing" ,
303
+ },
304
+ )
305
+ elif prev ["project" ] == cur ["project" ]:
306
+ # If the intervals are in the same project,
307
+ # we need to merge them into the same interval.
308
+ new_end = max (prev ["end" ], cur ["end" ])
309
+ prev ["end" ] = new_end
310
+ continue
311
+ else :
312
+ # This imples that there is some overlap between this transaction
313
+ # and the previous transaction. So we need push the interval up to
314
+ # the current item to the breakdown.
315
+
316
+ breakdown_append (
317
+ breakdown ,
318
+ {
319
+ "project" : prev ["project" ],
320
+ "start" : prev ["start" ],
321
+ "end" : cur ["start" ],
322
+ "kind" : "project" ,
323
+ },
324
+ )
325
+
326
+ if prev ["end" ] <= cur ["end" ]:
327
+ stack_pop (stack )
328
+
329
+ stack .append (cur )
330
+
331
+ for trace , stack in trace_stacks .items ():
332
+ breakdown = breakdowns_by_trace [trace ]
333
+ stack_clear (breakdown , stack )
334
+
335
+ return breakdowns_by_trace
0 commit comments