Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,17 +928,18 @@ def _capture_log(self, log):
if release is not None and "sentry.release" not in log["attributes"]:
log["attributes"]["sentry.release"] = release

span = current_scope.span
if span is not None and "sentry.trace.parent_span_id" not in log["attributes"]:
log["attributes"]["sentry.trace.parent_span_id"] = span.span_id

if log.get("trace_id") is None:
transaction = current_scope.transaction
propagation_context = isolation_scope.get_active_propagation_context()
if transaction is not None:
log["trace_id"] = transaction.trace_id
elif propagation_context is not None:
log["trace_id"] = propagation_context.trace_id
trace_context = current_scope.get_trace_context()
trace_id = trace_context.get("trace_id")
span_id = trace_context.get("span_id")

if trace_id is not None and log.get("trace_id") is None:
log["trace_id"] = trace_id

if (
span_id is not None
and "sentry.trace.parent_span_id" not in log["attributes"]
):
log["attributes"]["sentry.trace.parent_span_id"] = span_id

# The user, if present, is always set on the isolation scope.
if isolation_scope._user is not None:
Expand Down Expand Up @@ -977,6 +978,7 @@ def _capture_metric(self, metric):
if metric is None:
return

current_scope = sentry_sdk.get_current_scope()
isolation_scope = sentry_sdk.get_isolation_scope()

metric["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
Expand All @@ -990,16 +992,13 @@ def _capture_metric(self, metric):
if release is not None and "sentry.release" not in metric["attributes"]:
metric["attributes"]["sentry.release"] = release

span = sentry_sdk.get_current_span()
metric["trace_id"] = "00000000-0000-0000-0000-000000000000"
trace_context = current_scope.get_trace_context()
trace_id = trace_context.get("trace_id")
span_id = trace_context.get("span_id")

if span:
metric["trace_id"] = span.trace_id
metric["span_id"] = span.span_id
else:
propagation_context = isolation_scope.get_active_propagation_context()
if propagation_context and propagation_context.trace_id:
metric["trace_id"] = propagation_context.trace_id
metric["trace_id"] = trace_id or "00000000-0000-0000-0000-000000000000"
if span_id is not None:
metric["span_id"] = span_id

if isolation_scope._user is not None:
for metric_attribute, user_attribute in (
Expand Down
56 changes: 42 additions & 14 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@

global_event_processors = [] # type: List[EventProcessor]

# A function returning a (trace_id, span_id) tuple
# from an external tracing source (such as otel)
_external_propagation_context_fn = None # type: Optional[Callable[[], Optional[Tuple[str, str]]]]


class ScopeType(Enum):
CURRENT = "current"
Expand Down Expand Up @@ -142,6 +146,25 @@ def add_global_event_processor(processor):
global_event_processors.append(processor)


def register_external_propagation_context(fn):
# type: (Callable[[], Optional[Tuple[str, str]]]) -> None
global _external_propagation_context_fn
_external_propagation_context_fn = fn


def remove_external_propagation_context():
# type: () -> None
global _external_propagation_context_fn
_external_propagation_context_fn = None


def get_external_propagation_context():
# type: () -> Optional[Tuple[str, str]]
return (
_external_propagation_context_fn() if _external_propagation_context_fn else None
)


def _attr_setter(fn):
# type: (Any) -> Any
return property(fset=fn, doc=fn.__doc__)
Expand Down Expand Up @@ -562,21 +585,29 @@ def get_baggage(self, *args, **kwargs):
return self.get_isolation_scope().get_baggage()

def get_trace_context(self):
# type: () -> Any
# type: () -> Dict[str, Any]
"""
Returns the Sentry "trace" context from the Propagation Context.
"""
if self._propagation_context is None:
return None
if has_tracing_enabled(self.get_client().options) and self._span is not None:
return self._span.get_trace_context()

trace_context = {
"trace_id": self._propagation_context.trace_id,
"span_id": self._propagation_context.span_id,
"parent_span_id": self._propagation_context.parent_span_id,
"dynamic_sampling_context": self.get_dynamic_sampling_context(),
} # type: Dict[str, Any]
# if we are tracing externally (otel), those values take precedence
external_propagation_context = get_external_propagation_context()
if external_propagation_context:
trace_id, span_id = external_propagation_context
return {"trace_id": trace_id, "span_id": span_id}

return trace_context
propagation_context = self.get_active_propagation_context()
if propagation_context is None:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

propagation_context is actually never None, but this implementation is whack so we need this redundant check...

Copy link
Member Author

@sl0thentr0py sl0thentr0py Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for some future cleanup, if you see get_active_propagation_context that's also really shady. I generally dislike that so many instance methods on this scope call and fetch other scopes themselves, it's entirely spaghetti but somehow works in the common code paths.

return {}

return {
"trace_id": propagation_context.trace_id,
"span_id": propagation_context.span_id,
"parent_span_id": propagation_context.parent_span_id,
"dynamic_sampling_context": self.get_dynamic_sampling_context(),
}

def trace_propagation_meta(self, *args, **kwargs):
# type: (*Any, **Any) -> str
Expand Down Expand Up @@ -1438,10 +1469,7 @@ def _apply_contexts_to_event(self, event, hint, options):

# Add "trace" context
if contexts.get("trace") is None:
if has_tracing_enabled(options) and self._span is not None:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved these into get_trace_context for consistency

contexts["trace"] = self._span.get_trace_context()
else:
contexts["trace"] = self.get_trace_context()
contexts["trace"] = self.get_trace_context()

def _apply_flags_to_event(self, event, hint, options):
# type: (Event, Hint, Optional[Dict[str, Any]]) -> None
Expand Down
4 changes: 4 additions & 0 deletions tests/integrations/logging/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ def test_logger_with_all_attributes(sentry_init, capture_envelopes):

assert attributes.pop("sentry.sdk.name").startswith("sentry.python")

assert "sentry.trace.parent_span_id" in attributes
assert isinstance(attributes["sentry.trace.parent_span_id"], str)
del attributes["sentry.trace.parent_span_id"]

# Assert on the remaining non-dynamic attributes.
assert attributes == {
"foo": "bar",
Expand Down
4 changes: 4 additions & 0 deletions tests/integrations/loguru/test_loguru.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,10 @@ def test_logger_with_all_attributes(

assert attributes.pop("sentry.sdk.name").startswith("sentry.python")

assert "sentry.trace.parent_span_id" in attributes
assert isinstance(attributes["sentry.trace.parent_span_id"], str)
del attributes["sentry.trace.parent_span_id"]

# Assert on the remaining non-dynamic attributes.
assert attributes == {
"logger.name": "tests.integrations.loguru.test_loguru",
Expand Down
4 changes: 2 additions & 2 deletions tests/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def test_logs_tied_to_transactions(sentry_init, capture_envelopes):
"""
Log messages are also tied to transactions.
"""
sentry_init(enable_logs=True)
sentry_init(enable_logs=True, traces_sample_rate=1.0)
envelopes = capture_envelopes()

with sentry_sdk.start_transaction(name="test-transaction") as trx:
Expand All @@ -326,7 +326,7 @@ def test_logs_tied_to_spans(sentry_init, capture_envelopes):
"""
Log messages are also tied to spans.
"""
sentry_init(enable_logs=True)
sentry_init(enable_logs=True, traces_sample_rate=1.0)
envelopes = capture_envelopes()

with sentry_sdk.start_transaction(name="test-transaction"):
Expand Down
16 changes: 9 additions & 7 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def test_metrics_with_span(sentry_init, capture_envelopes):
sentry_init(traces_sample_rate=1.0)
envelopes = capture_envelopes()

with sentry_sdk.start_transaction(op="test", name="test-span"):
with sentry_sdk.start_transaction(op="test", name="test-span") as transaction:
sentry_sdk.metrics.count("test.span.counter", 1)

get_client().flush()
Expand All @@ -131,24 +131,26 @@ def test_metrics_with_span(sentry_init, capture_envelopes):
assert len(metrics) == 1

assert metrics[0]["trace_id"] is not None
assert metrics[0]["trace_id"] != "00000000-0000-0000-0000-000000000000"
assert metrics[0]["span_id"] is not None
assert metrics[0]["trace_id"] == transaction.trace_id
assert metrics[0]["span_id"] == transaction.span_id


def test_metrics_tracing_without_performance(sentry_init, capture_envelopes):
sentry_init()
envelopes = capture_envelopes()

sentry_sdk.metrics.count("test.span.counter", 1)
with sentry_sdk.isolation_scope() as isolation_scope:
sentry_sdk.metrics.count("test.span.counter", 1)

get_client().flush()

metrics = envelopes_to_metrics(envelopes)
assert len(metrics) == 1

assert metrics[0]["trace_id"] is not None
assert metrics[0]["trace_id"] != "00000000-0000-0000-0000-000000000000"
assert metrics[0]["span_id"] is None
propagation_context = isolation_scope._propagation_context
assert propagation_context is not None
assert metrics[0]["trace_id"] == propagation_context.trace_id
assert metrics[0]["span_id"] == propagation_context.span_id


def test_metrics_before_send(sentry_init, capture_envelopes):
Expand Down
46 changes: 46 additions & 0 deletions tests/test_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use_isolation_scope,
use_scope,
should_send_default_pii,
register_external_propagation_context,
remove_external_propagation_context,
)


Expand Down Expand Up @@ -971,3 +973,47 @@ def test_handle_error_on_token_reset_isolation_scope(error_cls, scope_manager):

mock_capture.assert_called_once()
mock_current_scope.reset.assert_called_once_with(mock_current_token)


def test_trace_context_tracing(sentry_init):
sentry_init(traces_sample_rate=1.0)

with sentry_sdk.start_transaction(name="trx") as transaction:
with sentry_sdk.start_span(op="span1"):
with sentry_sdk.start_span(op="span2") as span:
trace_context = sentry_sdk.get_current_scope().get_trace_context()

assert trace_context["trace_id"] == transaction.trace_id
assert trace_context["span_id"] == span.span_id
assert trace_context["parent_span_id"] == span.parent_span_id
assert "dynamic_sampling_context" in trace_context


def test_trace_context_external_tracing(sentry_init):
sentry_init()

def external_propagation_context():
return ("trace_id_foo", "span_id_bar")

register_external_propagation_context(external_propagation_context)

trace_context = sentry_sdk.get_current_scope().get_trace_context()

assert trace_context["trace_id"] == "trace_id_foo"
assert trace_context["span_id"] == "span_id_bar"

remove_external_propagation_context()


def test_trace_context_without_performance(sentry_init):
sentry_init()

with sentry_sdk.isolation_scope() as isolation_scope:
trace_context = sentry_sdk.get_current_scope().get_trace_context()

propagation_context = isolation_scope._propagation_context
assert propagation_context is not None
assert trace_context["trace_id"] == propagation_context.trace_id
assert trace_context["span_id"] == propagation_context.span_id
assert trace_context["parent_span_id"] == propagation_context.parent_span_id
assert "dynamic_sampling_context" in trace_context