Skip to content

Commit 121f86d

Browse files
committed
added duration argument to some tracing APIs (#550)
this fixes the breakdown tests on Windows by avoiding time.sleep(), which has an insufficient resolution on Windows. fixes #537 closes #550
1 parent b6aa09b commit 121f86d

File tree

4 files changed

+110
-68
lines changed

4 files changed

+110
-68
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
### Bugfixes
77
* fixed an issue with empty responses from APM Server's config endpoint (#562, #563)
8+
* fixed Windows tests by avoiding time.sleep in breakdown metrics tests (#537, #550)
89

910
## v5.1.0
1011
[Check the diff](https://github.com/elastic/apm-agent-python/compare/v5.0.0...v5.1.0)

elasticapm/base.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,27 @@ def queue(self, event_type, data, flush=False):
235235
flush = False
236236
self._transport.queue(event_type, data, flush)
237237

238-
def begin_transaction(self, transaction_type, trace_parent=None):
239-
"""Register the start of a transaction on the client
238+
def begin_transaction(self, transaction_type, trace_parent=None, start=None):
240239
"""
241-
return self.tracer.begin_transaction(transaction_type, trace_parent=trace_parent)
240+
Register the start of a transaction on the client
242241
243-
def end_transaction(self, name=None, result=""):
244-
transaction = self.tracer.end_transaction(result, name)
242+
:param transaction_type: type of the transaction, e.g. "request"
243+
:param trace_parent: an optional TraceParent object for distributed tracing
244+
:param start: override the start timestamp, mostly useful for testing
245+
:return: the started transaction object
246+
"""
247+
return self.tracer.begin_transaction(transaction_type, trace_parent=trace_parent, start=start)
248+
249+
def end_transaction(self, name=None, result="", duration=None):
250+
"""
251+
End the current transaction.
252+
253+
:param name: optional name of the transaction
254+
:param result: result of the transaction, e.g. "OK" or "HTTP 2xx"
255+
:param duration: override duration, mostly useful for testing
256+
:return: the ended transaction object
257+
"""
258+
transaction = self.tracer.end_transaction(result, name, duration=duration)
245259
return transaction
246260

247261
def close(self):

elasticapm/traces.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

136136
class 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

504534
class 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

Comments
 (0)