From f23f8b7b37225ffbe1ff998255c1585876e0b887 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Wed, 4 Dec 2024 21:55:16 +0100 Subject: [PATCH] Fix ThreadingIntegration by carrying forward span reference in `use_scope` Since the otel context span reference is the source of truth for the current active span, we need to explicitly pass the span reference on the scope through when we use `use_scope` since we are changing context variables in the other thread. Also, * fixes some typing in the original scope * adds more types to the `contextvars_context` manager --- MIGRATION_GUIDE.md | 1 + .../opentelemetry/contextvars_context.py | 32 +++++++++++++++++-- sentry_sdk/integrations/threading.py | 20 +++--------- sentry_sdk/scope.py | 7 ++-- sentry_sdk/tracing.py | 4 +-- .../integrations/threading/test_threading.py | 20 ++++++------ 6 files changed, 51 insertions(+), 33 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 1c0fa76fb0..6f0aeb4510 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -138,6 +138,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - `span.containing_transaction` has been removed. Use `span.root_span` instead. - `continue_from_headers`, `continue_from_environ` and `from_traceparent` have been removed, please use top-level API `sentry_sdk.continue_trace` instead. - `PropagationContext` constructor no longer takes a `dynamic_sampling_context` but takes a `baggage` object instead. +- `ThreadingIntegration` no longer takes the `propagate_hub` argument. ### Deprecated diff --git a/sentry_sdk/integrations/opentelemetry/contextvars_context.py b/sentry_sdk/integrations/opentelemetry/contextvars_context.py index b66b10d18a..8025f26ba8 100644 --- a/sentry_sdk/integrations/opentelemetry/contextvars_context.py +++ b/sentry_sdk/integrations/opentelemetry/contextvars_context.py @@ -1,3 +1,6 @@ +from typing import cast, TYPE_CHECKING + +from opentelemetry.trace import set_span_in_context from opentelemetry.context import Context, get_value, set_value from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext @@ -9,25 +12,50 @@ SENTRY_USE_ISOLATION_SCOPE_KEY, ) +if TYPE_CHECKING: + from typing import Optional + from sentry_sdk.integrations.opentelemetry.scope import PotelScope + class SentryContextVarsRuntimeContext(ContextVarsRuntimeContext): def attach(self, context): # type: (Context) -> object scopes = get_value(SENTRY_SCOPES_KEY, context) + should_fork_isolation_scope = context.pop( SENTRY_FORK_ISOLATION_SCOPE_KEY, False ) + should_fork_isolation_scope = cast("bool", should_fork_isolation_scope) + should_use_isolation_scope = context.pop(SENTRY_USE_ISOLATION_SCOPE_KEY, None) + should_use_isolation_scope = cast( + "Optional[PotelScope]", should_use_isolation_scope + ) + should_use_current_scope = context.pop(SENTRY_USE_CURRENT_SCOPE_KEY, None) + should_use_current_scope = cast( + "Optional[PotelScope]", should_use_current_scope + ) - if scopes and isinstance(scopes, tuple): + if scopes: + scopes = cast("tuple[PotelScope, PotelScope]", scopes) (current_scope, isolation_scope) = scopes else: current_scope = sentry_sdk.get_current_scope() isolation_scope = sentry_sdk.get_isolation_scope() + new_context = context + if should_use_current_scope: new_scope = should_use_current_scope + + # the main case where we use use_scope is for + # scope propagation in the ThreadingIntegration + # so we need to carry forward the span reference explicitly too + span = should_use_current_scope.span + if span: + new_context = set_span_in_context(span._otel_span, new_context) + else: new_scope = current_scope.fork() @@ -40,5 +68,5 @@ def attach(self, context): new_scopes = (new_scope, new_isolation_scope) - new_context = set_value(SENTRY_SCOPES_KEY, new_scopes, context) + new_context = set_value(SENTRY_SCOPES_KEY, new_scopes, new_context) return super().attach(new_context) diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py index 99bfc66611..33cdd0d0be 100644 --- a/sentry_sdk/integrations/threading.py +++ b/sentry_sdk/integrations/threading.py @@ -7,7 +7,6 @@ from sentry_sdk.utils import ( event_from_exception, capture_internal_exceptions, - logger, reraise, ) @@ -27,22 +26,10 @@ class ThreadingIntegration(Integration): identifier = "threading" - def __init__(self, propagate_hub=None, propagate_scope=True): - # type: (Optional[bool], bool) -> None - if propagate_hub is not None: - logger.warning( - "Deprecated: propagate_hub is deprecated. This will be removed in the future." - ) - - # Note: propagate_hub did not have any effect on propagation of scope data - # scope data was always propagated no matter what the value of propagate_hub was - # This is why the default for propagate_scope is True - + def __init__(self, propagate_scope=True): + # type: (bool) -> None self.propagate_scope = propagate_scope - if propagate_hub is not None: - self.propagate_scope = propagate_hub - @staticmethod def setup_once(): # type: () -> None @@ -99,7 +86,8 @@ def _run_old_run_func(): with sentry_sdk.use_scope(current_scope_to_use): return _run_old_run_func() else: - return _run_old_run_func() + with sentry_sdk.isolation_scope(): + return _run_old_run_func() return run # type: ignore diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 54e6fc8928..7083c3709c 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -26,6 +26,7 @@ SENTRY_TRACE_HEADER_NAME, NoOpSpan, Span, + POTelSpan, Transaction, ) from sentry_sdk.utils import ( @@ -669,7 +670,7 @@ def clear(self): self.clear_breadcrumbs() self._should_capture = True # type: bool - self._span = None # type: Optional[Span] + self._span = None # type: Optional[POTelSpan] self._session = None # type: Optional[Session] self._force_auto_session_tracking = None # type: Optional[bool] @@ -777,13 +778,13 @@ def set_user(self, value): @property def span(self): - # type: () -> Optional[Span] + # type: () -> Optional[POTelSpan] """Get current tracing span.""" return self._span @span.setter def span(self, span): - # type: (Optional[Span]) -> None + # type: (Optional[POTelSpan]) -> None """Set current tracing span.""" self._span = span diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 6728b9b4c9..7686dcf052 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1280,11 +1280,11 @@ def __eq__(self, other): def __repr__(self): # type: () -> str return ( - "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" + "<%s(op=%r, name:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>" % ( self.__class__.__name__, self.op, - self.description, + self.name, self.trace_id, self.span_id, self.parent_span_id, diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py index 0d14fae352..75b3b7eea1 100644 --- a/tests/integrations/threading/test_threading.py +++ b/tests/integrations/threading/test_threading.py @@ -35,11 +35,11 @@ def crash(): assert not events -@pytest.mark.parametrize("propagate_hub", (True, False)) -def test_propagates_hub(sentry_init, capture_events, propagate_hub): +@pytest.mark.parametrize("propagate_scope", (True, False)) +def test_propagates_scope(sentry_init, capture_events, propagate_scope): sentry_init( default_integrations=False, - integrations=[ThreadingIntegration(propagate_hub=propagate_hub)], + integrations=[ThreadingIntegration(propagate_scope=propagate_scope)], ) events = capture_events() @@ -65,25 +65,25 @@ def stage2(): assert exception["mechanism"]["type"] == "threading" assert not exception["mechanism"]["handled"] - if propagate_hub: + if propagate_scope: assert event["tags"]["stage1"] == "true" else: assert "stage1" not in event.get("tags", {}) -@pytest.mark.parametrize("propagate_hub", (True, False)) -def test_propagates_threadpool_hub(sentry_init, capture_events, propagate_hub): +@pytest.mark.parametrize("propagate_scope", (True, False)) +def test_propagates_threadpool_scope(sentry_init, capture_events, propagate_scope): sentry_init( traces_sample_rate=1.0, - integrations=[ThreadingIntegration(propagate_hub=propagate_hub)], + integrations=[ThreadingIntegration(propagate_scope=propagate_scope)], ) events = capture_events() def double(number): - with sentry_sdk.start_span(op="task", name=str(number)): + with sentry_sdk.start_span(op="task", name=str(number), only_if_parent=True): return number * 2 - with sentry_sdk.start_transaction(name="test_handles_threadpool"): + with sentry_sdk.start_span(name="test_handles_threadpool"): with futures.ThreadPoolExecutor(max_workers=1) as executor: tasks = [executor.submit(double, number) for number in [1, 2, 3, 4]] for future in futures.as_completed(tasks): @@ -91,7 +91,7 @@ def double(number): sentry_sdk.flush() - if propagate_hub: + if propagate_scope: assert len(events) == 1 (event,) = events assert event["spans"][0]["trace_id"] == event["spans"][1]["trace_id"]