Skip to content

Commit f5db9ce

Browse files
authored
Refactoring propagation context (#2970)
Create a class for the `PropagationContext`. Make the class generate the UUIDs lazily. Fixes #2827
1 parent d91a510 commit f5db9ce

File tree

6 files changed

+231
-91
lines changed

6 files changed

+231
-91
lines changed

sentry_sdk/scope.py

+27-78
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import sys
3-
import uuid
43
from copy import copy
54
from collections import deque
65
from contextlib import contextmanager
@@ -15,9 +14,9 @@
1514
from sentry_sdk.session import Session
1615
from sentry_sdk.tracing_utils import (
1716
Baggage,
18-
extract_sentrytrace_data,
1917
has_tracing_enabled,
2018
normalize_incoming_data,
19+
PropagationContext,
2120
)
2221
from sentry_sdk.tracing import (
2322
BAGGAGE_HEADER_NAME,
@@ -196,7 +195,7 @@ def __init__(self, ty=None, client=None):
196195
self._error_processors = [] # type: List[ErrorProcessor]
197196

198197
self._name = None # type: Optional[str]
199-
self._propagation_context = None # type: Optional[Dict[str, Any]]
198+
self._propagation_context = None # type: Optional[PropagationContext]
200199

201200
self.client = NonRecordingClient() # type: sentry_sdk.client.BaseClient
202201

@@ -431,77 +430,28 @@ def _load_trace_data_from_env(self):
431430

432431
return incoming_trace_information or None
433432

434-
def _extract_propagation_context(self, data):
435-
# type: (Dict[str, Any]) -> Optional[Dict[str, Any]]
436-
context = {} # type: Dict[str, Any]
437-
normalized_data = normalize_incoming_data(data)
438-
439-
baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME)
440-
if baggage_header:
441-
context["dynamic_sampling_context"] = Baggage.from_incoming_header(
442-
baggage_header
443-
).dynamic_sampling_context()
444-
445-
sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME)
446-
if sentry_trace_header:
447-
sentrytrace_data = extract_sentrytrace_data(sentry_trace_header)
448-
if sentrytrace_data is not None:
449-
context.update(sentrytrace_data)
450-
451-
only_baggage_no_sentry_trace = (
452-
"dynamic_sampling_context" in context and "trace_id" not in context
453-
)
454-
if only_baggage_no_sentry_trace:
455-
context.update(self._create_new_propagation_context())
456-
457-
if context:
458-
if not context.get("span_id"):
459-
context["span_id"] = uuid.uuid4().hex[16:]
460-
461-
return context
462-
463-
return None
464-
465-
def _create_new_propagation_context(self):
466-
# type: () -> Dict[str, Any]
467-
return {
468-
"trace_id": uuid.uuid4().hex,
469-
"span_id": uuid.uuid4().hex[16:],
470-
"parent_span_id": None,
471-
"dynamic_sampling_context": None,
472-
}
473-
474433
def set_new_propagation_context(self):
475434
# type: () -> None
476435
"""
477436
Creates a new propagation context and sets it as `_propagation_context`. Overwriting existing one.
478437
"""
479-
self._propagation_context = self._create_new_propagation_context()
480-
logger.debug(
481-
"[Tracing] Create new propagation context: %s",
482-
self._propagation_context,
483-
)
438+
self._propagation_context = PropagationContext()
484439

485440
def generate_propagation_context(self, incoming_data=None):
486441
# type: (Optional[Dict[str, str]]) -> None
487442
"""
488-
Makes sure the propagation context (`_propagation_context`) is set.
489-
The propagation context only lives on the current scope.
490-
If there is `incoming_data` overwrite existing `_propagation_context`.
491-
if there is no `incoming_data` create new `_propagation_context`, but do NOT overwrite if already existing.
443+
Makes sure the propagation context is set on the scope.
444+
If there is `incoming_data` overwrite existing propagation context.
445+
If there is no `incoming_data` create new propagation context, but do NOT overwrite if already existing.
492446
"""
493447
if incoming_data:
494-
context = self._extract_propagation_context(incoming_data)
495-
496-
if context is not None:
497-
self._propagation_context = context
498-
logger.debug(
499-
"[Tracing] Extracted propagation context from incoming data: %s",
500-
self._propagation_context,
501-
)
448+
propagation_context = PropagationContext.from_incoming_data(incoming_data)
449+
if propagation_context is not None:
450+
self._propagation_context = propagation_context
502451

503-
if self._propagation_context is None and self._type != ScopeType.CURRENT:
504-
self.set_new_propagation_context()
452+
if self._type != ScopeType.CURRENT:
453+
if self._propagation_context is None:
454+
self.set_new_propagation_context()
505455

506456
def get_dynamic_sampling_context(self):
507457
# type: () -> Optional[Dict[str, str]]
@@ -514,11 +464,11 @@ def get_dynamic_sampling_context(self):
514464

515465
baggage = self.get_baggage()
516466
if baggage is not None:
517-
self._propagation_context["dynamic_sampling_context"] = (
467+
self._propagation_context.dynamic_sampling_context = (
518468
baggage.dynamic_sampling_context()
519469
)
520470

521-
return self._propagation_context["dynamic_sampling_context"]
471+
return self._propagation_context.dynamic_sampling_context
522472

523473
def get_traceparent(self, *args, **kwargs):
524474
# type: (Any, Any) -> Optional[str]
@@ -535,8 +485,8 @@ def get_traceparent(self, *args, **kwargs):
535485
# If this scope has a propagation context, return traceparent from there
536486
if self._propagation_context is not None:
537487
traceparent = "%s-%s" % (
538-
self._propagation_context["trace_id"],
539-
self._propagation_context["span_id"],
488+
self._propagation_context.trace_id,
489+
self._propagation_context.span_id,
540490
)
541491
return traceparent
542492

@@ -557,8 +507,8 @@ def get_baggage(self, *args, **kwargs):
557507

558508
# If this scope has a propagation context, return baggage from there
559509
if self._propagation_context is not None:
560-
dynamic_sampling_context = self._propagation_context.get(
561-
"dynamic_sampling_context"
510+
dynamic_sampling_context = (
511+
self._propagation_context.dynamic_sampling_context
562512
)
563513
if dynamic_sampling_context is None:
564514
return Baggage.from_options(self)
@@ -577,9 +527,9 @@ def get_trace_context(self):
577527
return None
578528

579529
trace_context = {
580-
"trace_id": self._propagation_context["trace_id"],
581-
"span_id": self._propagation_context["span_id"],
582-
"parent_span_id": self._propagation_context["parent_span_id"],
530+
"trace_id": self._propagation_context.trace_id,
531+
"span_id": self._propagation_context.span_id,
532+
"parent_span_id": self._propagation_context.parent_span_id,
583533
"dynamic_sampling_context": self.get_dynamic_sampling_context(),
584534
} # type: Dict[str, Any]
585535

@@ -667,7 +617,7 @@ def iter_trace_propagation_headers(self, *args, **kwargs):
667617
yield header
668618

669619
def get_active_propagation_context(self):
670-
# type: () -> Dict[str, Any]
620+
# type: () -> Optional[PropagationContext]
671621
if self._propagation_context is not None:
672622
return self._propagation_context
673623

@@ -679,7 +629,7 @@ def get_active_propagation_context(self):
679629
if isolation_scope._propagation_context is not None:
680630
return isolation_scope._propagation_context
681631

682-
return {}
632+
return None
683633

684634
def clear(self):
685635
# type: () -> None
@@ -1069,12 +1019,11 @@ def start_span(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
10691019
span = self.span or Scope.get_isolation_scope().span
10701020

10711021
if span is None:
1072-
# New spans get the `trace_id`` from the scope
1022+
# New spans get the `trace_id` from the scope
10731023
if "trace_id" not in kwargs:
1074-
1075-
trace_id = self.get_active_propagation_context().get("trace_id")
1076-
if trace_id is not None:
1077-
kwargs["trace_id"] = trace_id
1024+
propagation_context = self.get_active_propagation_context()
1025+
if propagation_context is not None:
1026+
kwargs["trace_id"] = propagation_context.trace_id
10781027

10791028
span = Span(**kwargs)
10801029
else:

sentry_sdk/tracing_utils.py

+111-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from datetime import timedelta
88
from functools import wraps
99
from urllib.parse import quote, unquote
10+
import uuid
1011

1112
import sentry_sdk
1213
from sentry_sdk.consts import OP, SPANDATA
@@ -318,6 +319,109 @@ def _format_sql(cursor, sql):
318319
return real_sql or to_string(sql)
319320

320321

322+
class PropagationContext:
323+
"""
324+
The PropagationContext represents the data of a trace in Sentry.
325+
"""
326+
327+
__slots__ = (
328+
"_trace_id",
329+
"_span_id",
330+
"parent_span_id",
331+
"parent_sampled",
332+
"dynamic_sampling_context",
333+
)
334+
335+
def __init__(
336+
self,
337+
trace_id=None, # type: Optional[str]
338+
span_id=None, # type: Optional[str]
339+
parent_span_id=None, # type: Optional[str]
340+
parent_sampled=None, # type: Optional[bool]
341+
dynamic_sampling_context=None, # type: Optional[Dict[str, str]]
342+
):
343+
# type: (...) -> None
344+
self._trace_id = trace_id
345+
"""The trace id of the Sentry trace."""
346+
347+
self._span_id = span_id
348+
"""The span id of the currently executing span."""
349+
350+
self.parent_span_id = parent_span_id
351+
"""The id of the parent span that started this span.
352+
The parent span could also be a span in an upstream service."""
353+
354+
self.parent_sampled = parent_sampled
355+
"""Boolean indicator if the parent span was sampled.
356+
Important when the parent span originated in an upstream service,
357+
because we watn to sample the whole trace, or nothing from the trace."""
358+
359+
self.dynamic_sampling_context = dynamic_sampling_context
360+
"""Data that is used for dynamic sampling decisions."""
361+
362+
@classmethod
363+
def from_incoming_data(cls, incoming_data):
364+
# type: (Dict[str, Any]) -> Optional[PropagationContext]
365+
propagation_context = None
366+
367+
normalized_data = normalize_incoming_data(incoming_data)
368+
baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME)
369+
if baggage_header:
370+
propagation_context = PropagationContext()
371+
propagation_context.dynamic_sampling_context = Baggage.from_incoming_header(
372+
baggage_header
373+
).dynamic_sampling_context()
374+
375+
sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME)
376+
if sentry_trace_header:
377+
sentrytrace_data = extract_sentrytrace_data(sentry_trace_header)
378+
if sentrytrace_data is not None:
379+
if propagation_context is None:
380+
propagation_context = PropagationContext()
381+
propagation_context.update(sentrytrace_data)
382+
383+
return propagation_context
384+
385+
@property
386+
def trace_id(self):
387+
# type: () -> str
388+
"""The trace id of the Sentry trace."""
389+
if not self._trace_id:
390+
self._trace_id = uuid.uuid4().hex
391+
392+
return self._trace_id
393+
394+
@trace_id.setter
395+
def trace_id(self, value):
396+
# type: (str) -> None
397+
self._trace_id = value
398+
399+
@property
400+
def span_id(self):
401+
# type: () -> str
402+
"""The span id of the currently executed span."""
403+
if not self._span_id:
404+
self._span_id = uuid.uuid4().hex[16:]
405+
406+
return self._span_id
407+
408+
@span_id.setter
409+
def span_id(self, value):
410+
# type: (str) -> None
411+
self._span_id = value
412+
413+
def update(self, other_dict):
414+
# type: (Dict[str, Any]) -> None
415+
"""
416+
Updates the PropagationContext with data from the given dictionary.
417+
"""
418+
for key, value in other_dict.items():
419+
try:
420+
setattr(self, key, value)
421+
except AttributeError:
422+
pass
423+
424+
321425
class Baggage:
322426
"""
323427
The W3C Baggage header information (see https://www.w3.org/TR/baggage/).
@@ -381,8 +485,8 @@ def from_options(cls, scope):
381485
options = client.options
382486
propagation_context = scope._propagation_context
383487

384-
if propagation_context is not None and "trace_id" in propagation_context:
385-
sentry_items["trace_id"] = propagation_context["trace_id"]
488+
if propagation_context is not None:
489+
sentry_items["trace_id"] = propagation_context.trace_id
386490

387491
if options.get("environment"):
388492
sentry_items["environment"] = options["environment"]
@@ -568,7 +672,11 @@ def get_current_span(scope=None):
568672

569673

570674
# Circular imports
571-
from sentry_sdk.tracing import LOW_QUALITY_TRANSACTION_SOURCES
675+
from sentry_sdk.tracing import (
676+
BAGGAGE_HEADER_NAME,
677+
LOW_QUALITY_TRANSACTION_SOURCES,
678+
SENTRY_TRACE_HEADER_NAME,
679+
)
572680

573681
if TYPE_CHECKING:
574682
from sentry_sdk.tracing import Span

tests/integrations/celery/test_celery.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ def dummy_task(x, y):
154154

155155
assert (
156156
error_event["contexts"]["trace"]["trace_id"]
157-
== scope._propagation_context["trace_id"]
157+
== scope._propagation_context.trace_id
158158
)
159159
assert (
160160
error_event["contexts"]["trace"]["span_id"]
161-
!= scope._propagation_context["span_id"]
161+
!= scope._propagation_context.span_id
162162
)
163163
assert error_event["transaction"] == "dummy_task"
164164
assert "celery_task_id" in error_event["tags"]

tests/integrations/rq/test_rq.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def test_tracing_disabled(
190190
assert error_event["transaction"] == "tests.integrations.rq.test_rq.crashing_job"
191191
assert (
192192
error_event["contexts"]["trace"]["trace_id"]
193-
== scope._propagation_context["trace_id"]
193+
== scope._propagation_context.trace_id
194194
)
195195

196196

tests/test_api.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ def test_traceparent_with_tracing_disabled(sentry_init):
6666

6767
propagation_context = Scope.get_isolation_scope()._propagation_context
6868
expected_traceparent = "%s-%s" % (
69-
propagation_context["trace_id"],
70-
propagation_context["span_id"],
69+
propagation_context.trace_id,
70+
propagation_context.span_id,
7171
)
7272
assert get_traceparent() == expected_traceparent
7373

@@ -78,7 +78,7 @@ def test_baggage_with_tracing_disabled(sentry_init):
7878
propagation_context = Scope.get_isolation_scope()._propagation_context
7979
expected_baggage = (
8080
"sentry-trace_id={},sentry-environment=dev,sentry-release=1.0.0".format(
81-
propagation_context["trace_id"]
81+
propagation_context.trace_id
8282
)
8383
)
8484
assert get_baggage() == expected_baggage
@@ -112,10 +112,10 @@ def test_continue_trace(sentry_init):
112112
assert transaction.name == "some name"
113113

114114
propagation_context = Scope.get_isolation_scope()._propagation_context
115-
assert propagation_context["trace_id"] == transaction.trace_id == trace_id
116-
assert propagation_context["parent_span_id"] == parent_span_id
117-
assert propagation_context["parent_sampled"] == parent_sampled
118-
assert propagation_context["dynamic_sampling_context"] == {
115+
assert propagation_context.trace_id == transaction.trace_id == trace_id
116+
assert propagation_context.parent_span_id == parent_span_id
117+
assert propagation_context.parent_sampled == parent_sampled
118+
assert propagation_context.dynamic_sampling_context == {
119119
"trace_id": "566e3688a61d4bc888951642d6f14a19"
120120
}
121121

0 commit comments

Comments
 (0)