6
6
from datetime import datetime , timedelta
7
7
8
8
import sentry_sdk
9
+ from sentry_sdk .consts import INSTRUMENTER
9
10
from sentry_sdk .utils import logger
10
11
from sentry_sdk ._types import MYPY
11
12
@@ -125,6 +126,7 @@ def __init__(
125
126
status = None , # type: Optional[str]
126
127
transaction = None , # type: Optional[str] # deprecated
127
128
containing_transaction = None , # type: Optional[Transaction]
129
+ start_timestamp = None , # type: Optional[datetime]
128
130
):
129
131
# type: (...) -> None
130
132
self .trace_id = trace_id or uuid .uuid4 ().hex
@@ -139,7 +141,7 @@ def __init__(
139
141
self ._tags = {} # type: Dict[str, str]
140
142
self ._data = {} # type: Dict[str, Any]
141
143
self ._containing_transaction = containing_transaction
142
- self .start_timestamp = datetime .utcnow ()
144
+ self .start_timestamp = start_timestamp or datetime .utcnow ()
143
145
try :
144
146
# TODO: For Python 3.7+, we could use a clock with ns resolution:
145
147
# self._start_timestamp_monotonic = time.perf_counter_ns()
@@ -206,15 +208,22 @@ def containing_transaction(self):
206
208
# referencing themselves)
207
209
return self ._containing_transaction
208
210
209
- def start_child (self , ** kwargs ):
210
- # type: (**Any) -> Span
211
+ def start_child (self , instrumenter = INSTRUMENTER . SENTRY , ** kwargs ):
212
+ # type: (str, **Any) -> Span
211
213
"""
212
214
Start a sub-span from the current span or transaction.
213
215
214
216
Takes the same arguments as the initializer of :py:class:`Span`. The
215
217
trace id, sampling decision, transaction pointer, and span recorder are
216
218
inherited from the current span/transaction.
217
219
"""
220
+ hub = self .hub or sentry_sdk .Hub .current
221
+ client = hub .client
222
+ configuration_instrumenter = client and client .options ["instrumenter" ]
223
+
224
+ if instrumenter != configuration_instrumenter :
225
+ return NoOpSpan ()
226
+
218
227
kwargs .setdefault ("sampled" , self .sampled )
219
228
220
229
child = Span (
@@ -461,8 +470,8 @@ def is_success(self):
461
470
# type: () -> bool
462
471
return self .status == "ok"
463
472
464
- def finish (self , hub = None ):
465
- # type: (Optional[sentry_sdk.Hub]) -> Optional[str]
473
+ def finish (self , hub = None , end_timestamp = None ):
474
+ # type: (Optional[sentry_sdk.Hub], Optional[datetime] ) -> Optional[str]
466
475
# XXX: would be type: (Optional[sentry_sdk.Hub]) -> None, but that leads
467
476
# to incompatible return types for Span.finish and Transaction.finish.
468
477
if self .timestamp is not None :
@@ -472,8 +481,13 @@ def finish(self, hub=None):
472
481
hub = hub or self .hub or sentry_sdk .Hub .current
473
482
474
483
try :
475
- duration_seconds = time .perf_counter () - self ._start_timestamp_monotonic
476
- self .timestamp = self .start_timestamp + timedelta (seconds = duration_seconds )
484
+ if end_timestamp :
485
+ self .timestamp = end_timestamp
486
+ else :
487
+ duration_seconds = time .perf_counter () - self ._start_timestamp_monotonic
488
+ self .timestamp = self .start_timestamp + timedelta (
489
+ seconds = duration_seconds
490
+ )
477
491
except AttributeError :
478
492
self .timestamp = datetime .utcnow ()
479
493
@@ -550,6 +564,7 @@ class Transaction(Span):
550
564
# tracestate data from other vendors, of the form `dogs=yes,cats=maybe`
551
565
"_third_party_tracestate" ,
552
566
"_measurements" ,
567
+ "_contexts" ,
553
568
"_profile" ,
554
569
"_baggage" ,
555
570
"_active_thread_id" ,
@@ -575,7 +590,9 @@ def __init__(
575
590
"instead of Span(transaction=...)."
576
591
)
577
592
name = kwargs .pop ("transaction" )
593
+
578
594
Span .__init__ (self , ** kwargs )
595
+
579
596
self .name = name
580
597
self .source = source
581
598
self .sample_rate = None # type: Optional[float]
@@ -586,6 +603,7 @@ def __init__(
586
603
self ._sentry_tracestate = sentry_tracestate
587
604
self ._third_party_tracestate = third_party_tracestate
588
605
self ._measurements = {} # type: Dict[str, Any]
606
+ self ._contexts = {} # type: Dict[str, Any]
589
607
self ._profile = None # type: Optional[sentry_sdk.profiler.Profile]
590
608
self ._baggage = baggage
591
609
# for profiling, we want to know on which thread a transaction is started
@@ -619,8 +637,8 @@ def containing_transaction(self):
619
637
# reference.
620
638
return self
621
639
622
- def finish (self , hub = None ):
623
- # type: (Optional[sentry_sdk.Hub]) -> Optional[str]
640
+ def finish (self , hub = None , end_timestamp = None ):
641
+ # type: (Optional[sentry_sdk.Hub], Optional[datetime] ) -> Optional[str]
624
642
if self .timestamp is not None :
625
643
# This transaction is already finished, ignore.
626
644
return None
@@ -652,7 +670,7 @@ def finish(self, hub=None):
652
670
)
653
671
self .name = "<unlabeled transaction>"
654
672
655
- Span .finish (self , hub )
673
+ Span .finish (self , hub , end_timestamp )
656
674
657
675
if not self .sampled :
658
676
# At this point a `sampled = None` should have already been resolved
@@ -674,11 +692,15 @@ def finish(self, hub=None):
674
692
# to be garbage collected
675
693
self ._span_recorder = None
676
694
695
+ contexts = {}
696
+ contexts .update (self ._contexts )
697
+ contexts .update ({"trace" : self .get_trace_context ()})
698
+
677
699
event = {
678
700
"type" : "transaction" ,
679
701
"transaction" : self .name ,
680
702
"transaction_info" : {"source" : self .source },
681
- "contexts" : { "trace" : self . get_trace_context ()} ,
703
+ "contexts" : contexts ,
682
704
"tags" : self ._tags ,
683
705
"timestamp" : self .timestamp ,
684
706
"start_timestamp" : self .start_timestamp ,
@@ -703,6 +725,10 @@ def set_measurement(self, name, value, unit=""):
703
725
704
726
self ._measurements [name ] = {"value" : value , "unit" : unit }
705
727
728
+ def set_context (self , key , value ):
729
+ # type: (str, Any) -> None
730
+ self ._contexts [key ] = value
731
+
706
732
def to_json (self ):
707
733
# type: () -> Dict[str, Any]
708
734
rv = super (Transaction , self ).to_json ()
@@ -828,6 +854,48 @@ def _set_initial_sampling_decision(self, sampling_context):
828
854
)
829
855
830
856
857
+ class NoOpSpan (Span ):
858
+ def __repr__ (self ):
859
+ # type: () -> Any
860
+ return self .__class__ .__name__
861
+
862
+ def __enter__ (self ):
863
+ # type: () -> Any
864
+ return self
865
+
866
+ def __exit__ (self , ty , value , tb ):
867
+ # type: (Any, Any, Any) -> Any
868
+ pass
869
+
870
+ def start_child (self , instrumenter = INSTRUMENTER .SENTRY , ** kwargs ):
871
+ # type: (str, **Any) -> Any
872
+ pass
873
+
874
+ def new_span (self , ** kwargs ):
875
+ # type: (**Any) -> Any
876
+ pass
877
+
878
+ def set_tag (self , key , value ):
879
+ # type: (Any, Any) -> Any
880
+ pass
881
+
882
+ def set_data (self , key , value ):
883
+ # type: (Any, Any) -> Any
884
+ pass
885
+
886
+ def set_status (self , value ):
887
+ # type: (Any) -> Any
888
+ pass
889
+
890
+ def set_http_status (self , http_status ):
891
+ # type: (Any) -> Any
892
+ pass
893
+
894
+ def finish (self , hub = None , end_timestamp = None ):
895
+ # type: (Any, Any) -> Any
896
+ pass
897
+
898
+
831
899
# Circular imports
832
900
833
901
from sentry_sdk .tracing_utils import (
0 commit comments