Skip to content
Draft
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
96 changes: 62 additions & 34 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,54 +570,77 @@ def get_dynamic_sampling_context(self) -> "Optional[Dict[str, str]]":

def get_traceparent(self, *args: "Any", **kwargs: "Any") -> "Optional[str]":
"""
Returns the Sentry "sentry-trace" header (aka the traceparent) from the
currently active span or the scopes Propagation Context.
Returns the Sentry "sentry-trace" header from the Propagation Context.
"""
client = self.get_client()
propagation_context = self.get_active_propagation_context()

# Get sampled from current span if available (span.sampled may change after entering)
sampled = propagation_context.sampled
if self.span is not None:
sampled = self.span.sampled

if sampled is True:
sampled_str = "1"
elif sampled is False:
sampled_str = "0"
else:
sampled_str = None

# If we have an active span, return traceparent from there
if has_tracing_enabled(client.options) and self.span is not None:
return self.span.to_traceparent()
traceparent = f"{propagation_context.trace_id}-{propagation_context.span_id}"
if sampled_str is not None:
traceparent += f"-{sampled_str}"

# else return traceparent from the propagation context
return self.get_active_propagation_context().to_traceparent()
return traceparent

def get_baggage(self, *args: "Any", **kwargs: "Any") -> "Optional[Baggage]":
"""
Returns the Sentry "baggage" header containing trace information from the
currently active span or the scopes Propagation Context.
Returns the Sentry "baggage" header from the Propagation Context.
"""
client = self.get_client()

# If we have an active span, return baggage from there
if has_tracing_enabled(client.options) and self.span is not None:
return self.span.to_baggage()

# else return baggage from the propagation context
return self.get_active_propagation_context().get_baggage()

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

# if we are tracing externally (otel), those values take precedence
# External propagation context (OTel) takes 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}

propagation_context = self.get_active_propagation_context()

return {
# Base context from PropagationContext
rv: "Dict[str, Any]" = {
"trace_id": propagation_context.trace_id,
"span_id": propagation_context.span_id,
"parent_span_id": propagation_context.parent_span_id,
"dynamic_sampling_context": propagation_context.dynamic_sampling_context,
}

# Add additional context from the current span if available
if self.span is not None:
rv["parent_span_id"] = self.span.parent_span_id
rv["op"] = self.span.op
rv["description"] = self.span.description
rv["origin"] = self.span.origin
if self.span.status:
rv["status"] = self.span.status
# Add thread data if available
data = {}
from sentry_sdk.consts import SPANDATA

thread_id = self.span._data.get(SPANDATA.THREAD_ID)
if thread_id is not None:
data["thread.id"] = thread_id
thread_name = self.span._data.get(SPANDATA.THREAD_NAME)
if thread_name is not None:
data["thread.name"] = thread_name
if data:
rv["data"] = data

return rv

def trace_propagation_meta(self, *args: "Any", **kwargs: "Any") -> str:
"""
Return meta tags which should be injected into HTML templates
Expand Down Expand Up @@ -648,10 +671,7 @@ def iter_trace_propagation_headers(
self, *args: "Any", **kwargs: "Any"
) -> "Generator[Tuple[str, str], None, None]":
"""
Return HTTP headers which allow propagation of trace data.

If a span is given, the trace data will taken from the span.
If no span is given, the trace data is taken from the scope.
Return HTTP headers for trace propagation.
"""
client = self.get_client()
if not client.options.get("propagate_traces"):
Expand All @@ -663,18 +683,26 @@ def iter_trace_propagation_headers(
return

span = kwargs.pop("span", None)
span = span or self.span

if has_tracing_enabled(client.options) and span is not None:
# When using external propagation (OTel), leave to external propagator
if has_external_propagation_context():
return

# If a span is explicitly passed, use that span's headers for backwards compatibility
# This is needed for integrations that create spans but don't use them as context managers
if span is not None:
for header in span.iter_headers():
yield header
elif has_external_propagation_context():
# when we have an external_propagation_context (otlp)
# we leave outgoing propagation to the propagator
return
else:
for header in self.get_active_propagation_context().iter_headers():
yield header

# Otherwise, use PropagationContext (with sampled from current span if available)
yield SENTRY_TRACE_HEADER_NAME, self.get_traceparent()

baggage = self.get_baggage()
if baggage is not None:
serialized_baggage = baggage.serialize()
if serialized_baggage:
yield BAGGAGE_HEADER_NAME, serialized_baggage

def get_active_propagation_context(self) -> "PropagationContext":
if self._propagation_context is not None:
Expand Down
28 changes: 26 additions & 2 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,20 @@ def __enter__(self) -> "Span":
scope = self.scope or sentry_sdk.get_current_scope()
old_span = scope.span
scope.span = self
self._context_manager_state = (scope, old_span)

# Sync PropagationContext with the new active span
# Use the same PropagationContext that get_active_propagation_context() would return
propagation_context = scope.get_active_propagation_context()
old_propagation_context_state = (
propagation_context._trace_id,
propagation_context._span_id,
propagation_context.sampled,
)
propagation_context._trace_id = self.trace_id
propagation_context._span_id = self.span_id
propagation_context.sampled = self.sampled

self._context_manager_state = (scope, old_span, propagation_context, old_propagation_context_state)
return self

def __exit__(
Expand All @@ -400,11 +413,17 @@ def __exit__(
self.set_status(SPANSTATUS.INTERNAL_ERROR)

with capture_internal_exceptions():
scope, old_span = self._context_manager_state
scope, old_span, propagation_context, old_propagation_context_state = self._context_manager_state
del self._context_manager_state
self.finish(scope)
scope.span = old_span

# Restore PropagationContext state
old_trace_id, old_span_id, old_sampled = old_propagation_context_state
propagation_context._trace_id = old_trace_id
propagation_context._span_id = old_span_id
propagation_context.sampled = old_sampled

@property
def containing_transaction(self) -> "Optional[Transaction]":
"""The ``Transaction`` that this span belongs to.
Expand Down Expand Up @@ -858,6 +877,11 @@ def __enter__(self) -> "Transaction":

super().__enter__()

# Sync baggage to PropagationContext
isolation_scope = sentry_sdk.get_isolation_scope()
if isolation_scope._propagation_context is not None:
isolation_scope._propagation_context.baggage = self.get_baggage()

if self._profile is not None:
self._profile.__enter__()

Expand Down
21 changes: 19 additions & 2 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ class PropagationContext:
"_span_id",
"parent_span_id",
"parent_sampled",
"sampled",
"baggage",
)

Expand All @@ -414,6 +415,7 @@ def __init__(
span_id: "Optional[str]" = None,
parent_span_id: "Optional[str]" = None,
parent_sampled: "Optional[bool]" = None,
sampled: "Optional[bool]" = None,
dynamic_sampling_context: "Optional[Dict[str, str]]" = None,
baggage: "Optional[Baggage]" = None,
) -> None:
Expand All @@ -432,6 +434,9 @@ def __init__(
Important when the parent span originated in an upstream service,
because we want to sample the whole trace, or nothing from the trace."""

self.sampled = sampled
"""Boolean indicator if the current span is sampled."""

self.baggage = baggage
"""Parsed baggage header that is used for dynamic sampling decisions."""

Expand Down Expand Up @@ -499,7 +504,18 @@ def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]":
return self.get_baggage().dynamic_sampling_context()

def to_traceparent(self) -> str:
return f"{self.trace_id}-{self.span_id}"
if self.sampled is True:
sampled = "1"
elif self.sampled is False:
sampled = "0"
else:
sampled = None

traceparent = f"{self.trace_id}-{self.span_id}"
if sampled is not None:
traceparent += f"-{sampled}"

return traceparent

def get_baggage(self) -> "Baggage":
if self.baggage is None:
Expand Down Expand Up @@ -527,11 +543,12 @@ def update(self, other_dict: "Dict[str, Any]") -> None:
pass

def __repr__(self) -> str:
return "<PropagationContext _trace_id={} _span_id={} parent_span_id={} parent_sampled={} baggage={}>".format(
return "<PropagationContext _trace_id={} _span_id={} parent_span_id={} parent_sampled={} sampled={} baggage={}>".format(
self._trace_id,
self._span_id,
self.parent_span_id,
self.parent_sampled,
self.sampled,
self.baggage,
)

Expand Down
Loading