@@ -97,7 +97,7 @@ def child_started(self, timestamp):
9797 def child_ended (self , timestamp ):
9898 self ._child_durations .stop (timestamp )
9999
100- def end (self , skip_frames = 0 ):
100+ def end (self , skip_frames = 0 , duration = None ):
101101 raise NotImplementedError ()
102102
103103 def label (self , ** labels ):
@@ -134,10 +134,13 @@ def tag(self, **tags):
134134
135135
136136class Transaction (BaseSpan ):
137- def __init__ (self , tracer , transaction_type = "custom" , trace_parent = None , is_sampled = True ):
137+ def __init__ (self , tracer , transaction_type = "custom" , trace_parent = None , is_sampled = True , start = None ):
138138 self .id = "%016x" % random .getrandbits (64 )
139139 self .trace_parent = trace_parent
140- self .timestamp , self .start_time = time .time (), _time_func ()
140+ if start :
141+ self .timestamp = self .start_time = start
142+ else :
143+ self .timestamp , self .start_time = time .time (), _time_func ()
141144 self .name = None
142145 self .duration = None
143146 self .result = None
@@ -165,8 +168,8 @@ def __init__(self, tracer, transaction_type="custom", trace_parent=None, is_samp
165168 self ._transaction_metrics = None
166169 super (Transaction , self ).__init__ ()
167170
168- def end (self , skip_frames = 0 ):
169- self .duration = _time_func () - self .start_time
171+ def end (self , skip_frames = 0 , duration = None ):
172+ self .duration = duration if duration is not None else ( _time_func () - self .start_time )
170173 if self ._transaction_metrics :
171174 self ._transaction_metrics .timer (
172175 "transaction.duration" ,
@@ -202,6 +205,7 @@ def _begin_span(
202205 parent_span_id = None ,
203206 span_subtype = None ,
204207 span_action = None ,
208+ start = None ,
205209 ):
206210 parent_span = execution_context .get_span ()
207211 tracer = self .tracer
@@ -223,13 +227,16 @@ def _begin_span(
223227 parent_span_id = parent_span_id ,
224228 span_subtype = span_subtype ,
225229 span_action = span_action ,
230+ start = start ,
226231 )
227232 span .frames = tracer .frames_collector_func ()
228233 self ._span_counter += 1
229234 execution_context .set_span (span )
230235 return span
231236
232- def begin_span (self , name , span_type , context = None , leaf = False , labels = None , span_subtype = None , span_action = None ):
237+ def begin_span (
238+ self , name , span_type , context = None , leaf = False , labels = None , span_subtype = None , span_action = None , start = None
239+ ):
233240 """
234241 Begin a new span
235242 :param name: name of the span
@@ -239,6 +246,7 @@ def begin_span(self, name, span_type, context=None, leaf=False, labels=None, spa
239246 :param labels: a flat string/string dict of labels
240247 :param span_subtype: sub type of the span, e.g. "postgresql"
241248 :param span_action: action of the span , e.g. "query"
249+ :param start: timestamp, mostly useful for testing
242250 :return: the Span object
243251 """
244252 return self ._begin_span (
@@ -250,19 +258,21 @@ def begin_span(self, name, span_type, context=None, leaf=False, labels=None, spa
250258 parent_span_id = None ,
251259 span_subtype = span_subtype ,
252260 span_action = span_action ,
261+ start = start ,
253262 )
254263
255- def end_span (self , skip_frames = 0 ):
264+ def end_span (self , skip_frames = 0 , duration = None ):
256265 """
257266 End the currently active span
258267 :param skip_frames: numbers of frames to skip in the stack trace
268+ :param duration: override duration, mostly useful for testing
259269 :return: the ended span
260270 """
261271 span = execution_context .get_span ()
262272 if span is None :
263273 raise LookupError ()
264274
265- span .end (skip_frames = skip_frames )
275+ span .end (skip_frames = skip_frames , duration = duration )
266276 return span
267277
268278 def ensure_parent_id (self ):
@@ -337,6 +347,7 @@ def __init__(
337347 parent_span_id = None ,
338348 span_subtype = None ,
339349 span_action = None ,
350+ start = None ,
340351 ):
341352 """
342353 Create a new Span
@@ -350,8 +361,9 @@ def __init__(
350361 :param parent_span_id: override of the span ID
351362 :param span_subtype: sub type of the span, e.g. mysql
352363 :param span_action: sub type of the span, e.g. query
364+ :param start: timestamp, mostly useful for testing
353365 """
354- self .start_time = _time_func ()
366+ self .start_time = start or _time_func ()
355367 self .id = "%016x" % random .getrandbits (64 )
356368 self .transaction = transaction
357369 self .name = name
@@ -405,10 +417,17 @@ def to_dict(self):
405417 result ["stacktrace" ] = self .frames
406418 return result
407419
408- def end (self , skip_frames = 0 ):
420+ def end (self , skip_frames = 0 , duration = None ):
421+ """
422+ End this span and queue it for sending.
423+
424+ :param skip_frames: amount of frames to skip from the beginning of the stack trace
425+ :param duration: override duration, mostly useful for testing
426+ :return: None
427+ """
409428 tracer = self .transaction .tracer
410429 timestamp = _time_func ()
411- self .duration = timestamp - self .start_time
430+ self .duration = duration if duration is not None else ( timestamp - self .start_time )
412431 if not tracer .span_frames_min_duration or self .duration >= tracer .span_frames_min_duration :
413432 self .frames = tracer .frames_processing_func (self .frames )[skip_frames :]
414433 else :
@@ -417,7 +436,7 @@ def end(self, skip_frames=0):
417436 tracer .queue_func (SPAN , self .to_dict ())
418437 if self .transaction ._breakdown :
419438 p = self .parent if self .parent else self .transaction
420- p .child_ended (timestamp )
439+ p .child_ended (self . start_time + self . duration )
421440 self .transaction .track_span_duration (
422441 self .type , self .subtype , self .duration - self ._child_durations .duration
423442 )
@@ -434,7 +453,7 @@ def __init__(self, parent, leaf=False):
434453 self .leaf = leaf
435454 super (DroppedSpan , self ).__init__ ()
436455
437- def end (self , skip_frames = 0 ):
456+ def end (self , skip_frames = 0 , duration = None ):
438457 execution_context .set_span (self .parent )
439458
440459 def child_started (self , timestamp ):
@@ -458,10 +477,14 @@ def __init__(self, frames_collector_func, frames_processing_func, queue_func, co
458477 else :
459478 self .span_frames_min_duration = config .span_frames_min_duration / 1000.0
460479
461- def begin_transaction (self , transaction_type , trace_parent = None ):
480+ def begin_transaction (self , transaction_type , trace_parent = None , start = None ):
462481 """
463482 Start a new transactions and bind it in a thread-local variable
464483
484+ :param transaction_type: type of the transaction, e.g. "request"
485+ :param trace_parent: an optional TraceParent object
486+ :param start: override the start timestamp, mostly useful for testing
487+
465488 :returns the Transaction object
466489 """
467490 if trace_parent :
@@ -470,7 +493,7 @@ def begin_transaction(self, transaction_type, trace_parent=None):
470493 is_sampled = (
471494 self .config .transaction_sample_rate == 1.0 or self .config .transaction_sample_rate > random .random ()
472495 )
473- transaction = Transaction (self , transaction_type , trace_parent = trace_parent , is_sampled = is_sampled )
496+ transaction = Transaction (self , transaction_type , trace_parent = trace_parent , is_sampled = is_sampled , start = start )
474497 if trace_parent is None :
475498 transaction .trace_parent = TraceParent (
476499 constants .TRACE_CONTEXT_VERSION ,
@@ -487,12 +510,19 @@ def _should_ignore(self, transaction_name):
487510 return True
488511 return False
489512
490- def end_transaction (self , result = None , transaction_name = None ):
513+ def end_transaction (self , result = None , transaction_name = None , duration = None ):
514+ """
515+ End the current transaction and queue it for sending
516+ :param result: result of the transaction, e.g. "OK" or 200
517+ :param transaction_name: name of the transaction
518+ :param duration: override duration, mostly useful for testing
519+ :return:
520+ """
491521 transaction = execution_context .get_transaction (clear = True )
492522 if transaction :
493523 if transaction .name is None :
494524 transaction .name = transaction_name if transaction_name is not None else ""
495- transaction .end ()
525+ transaction .end (duration = duration )
496526 if self ._should_ignore (transaction .name ):
497527 return
498528 if transaction .result is None :
@@ -502,7 +532,7 @@ def end_transaction(self, result=None, transaction_name=None):
502532
503533
504534class capture_span (object ):
505- __slots__ = ("name" , "type" , "subtype" , "action" , "extra" , "skip_frames" , "leaf" , "labels" )
535+ __slots__ = ("name" , "type" , "subtype" , "action" , "extra" , "skip_frames" , "leaf" , "labels" , "duration" , "start" )
506536
507537 def __init__ (
508538 self ,
@@ -515,6 +545,8 @@ def __init__(
515545 labels = None ,
516546 span_subtype = None ,
517547 span_action = None ,
548+ start = None ,
549+ duration = None ,
518550 ):
519551 self .name = name
520552 self .type = span_type
@@ -532,6 +564,8 @@ def __init__(
532564 labels = tags
533565
534566 self .labels = labels
567+ self .start = start
568+ self .duration = duration
535569
536570 def __call__ (self , func ):
537571 self .name = self .name or get_name_from_func (func )
@@ -554,13 +588,14 @@ def __enter__(self):
554588 labels = self .labels ,
555589 span_subtype = self .subtype ,
556590 span_action = self .action ,
591+ start = self .start ,
557592 )
558593
559594 def __exit__ (self , exc_type , exc_val , exc_tb ):
560595 transaction = execution_context .get_transaction ()
561596 if transaction and transaction .is_sampled :
562597 try :
563- transaction .end_span (self .skip_frames )
598+ transaction .end_span (self .skip_frames , duration = self . duration )
564599 except LookupError :
565600 logger .info ("ended non-existing span %s of type %s" , self .name , self .type )
566601
0 commit comments