Skip to content
Open
53 changes: 52 additions & 1 deletion sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
normalize_incoming_data,
PropagationContext,
)
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.traces import StreamedSpan, NoOpStreamedSpan
from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
SENTRY_TRACE_HEADER_NAME,
Expand Down Expand Up @@ -1174,6 +1174,57 @@

return span

def start_streamed_span(
self,
name: str,
attributes: "Optional[Attributes]" = None,
parent_span: "Optional[StreamedSpan]" = None,
active: bool = True,
) -> "StreamedSpan":
# TODO: rename to start_span once we drop the old API
if isinstance(parent_span, NoOpStreamedSpan):
# parent_span is only set if the user explicitly set it
logger.debug(
"Ignored parent span provided. Span will be parented to the "
"currently active span instead."
)

if parent_span is None or isinstance(parent_span, NoOpStreamedSpan):
parent_span = self.span # type: ignore

# If no eligible parent_span was provided and there is no currently
# active span, this is a segment
if parent_span is None:
propagation_context = self.get_active_propagation_context()

return StreamedSpan(
name=name,
attributes=attributes,
active=active,
scope=self,
segment=None,
trace_id=propagation_context.trace_id,
parent_span_id=propagation_context.parent_span_id,
parent_sampled=propagation_context.parent_sampled,
baggage=propagation_context.baggage,
)

# This is a child span; take propagation context from the parent span
with new_scope():
if isinstance(parent_span, NoOpStreamedSpan):
return NoOpStreamedSpan()

Check warning on line 1215 in sentry_sdk/scope.py

View workflow job for this annotation

GitHub Actions / warden: code-review

Unreachable code: NoOpStreamedSpan check after reassignment will never be true

The condition `if isinstance(parent_span, NoOpStreamedSpan)` at line 1214 is unreachable dead code. At line 1192-1193, if `parent_span` is `None` or `NoOpStreamedSpan`, it gets reassigned to `self.span`. The code block starting at line 1213 is only reached when `parent_span is not None` (checked at line 1197), meaning `self.span` returned a non-None value. Since `self.span` cannot return a `NoOpStreamedSpan`, this check will never be true and lines 1214-1215 are unreachable.

return StreamedSpan(
name=name,
attributes=attributes,
active=active,
scope=self,
segment=parent_span._segment,
trace_id=parent_span.trace_id,
parent_span_id=parent_span.span_id,
parent_sampled=parent_span.sampled,
)

def continue_trace(
self,
environ_or_headers: "Dict[str, Any]",
Expand Down
97 changes: 96 additions & 1 deletion sentry_sdk/traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from enum import Enum
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.tracing_utils import Baggage
from sentry_sdk.utils import format_attribute, logger

if TYPE_CHECKING:
Expand Down Expand Up @@ -57,6 +59,66 @@ def __str__(self) -> str:
}


def start_span(
name: str,
attributes: "Optional[Attributes]" = None,
parent_span: "Optional[StreamedSpan]" = None,
active: bool = True,
) -> "StreamedSpan":
"""
Start a span.

The span's parent, unless provided explicitly via the `parent_span` argument,
will be the current active span, if any. If there is none, this span will
become the root of a new span tree.

`start_span()` can either be used as context manager or you can use the span
object it returns and explicitly end it via `span.end()`. The following is
equivalent:

```python
import sentry_sdk

with sentry_sdk.traces.start_span(name="My Span"):
# do something

# The span automatically finishes once the `with` block is exited
```

```python
import sentry_sdk

span = sentry_sdk.traces.start_span(name="My Span")
# do something
span.end()
```

:param name: The name to identify this span by.
:type name: str

:param attributes: Key-value attributes to set on the span from the start.
These will also be accessible in the traces sampler.
:type attributes: "Optional[Attributes]"

:param parent_span: A span instance that the new span should consider its
parent. If not provided, the parent will be set to the currently active
span, if any.
:type parent_span: "Optional[StreamedSpan]"

:param active: Controls whether spans started while this span is running
will automatically become its children. That's the default behavior. If
you want to create a span that shouldn't have any children (unless
provided explicitly via the `parent_span` argument), set this to `False`.
:type active: bool

:return: The span that has been started.
:rtype: StreamedSpan
"""
return sentry_sdk.get_current_scope().start_streamed_span(
name, attributes, parent_span, active
)


class StreamedSpan:
"""
A span holds timing information of a block of code.
Expand All @@ -73,7 +135,12 @@ class StreamedSpan:
"_active",
"_span_id",
"_trace_id",
"_parent_span_id",
"_segment",
"_parent_sampled",
"_status",
"_scope",
"_baggage",
)

def __init__(
Expand All @@ -82,7 +149,12 @@ def __init__(
name: str,
attributes: "Optional[Attributes]" = None,
active: bool = True,
scope: "sentry_sdk.Scope",
segment: "Optional[StreamedSpan]" = None,
trace_id: "Optional[str]" = None,
parent_span_id: "Optional[str]" = None,
parent_sampled: "Optional[bool]" = None,
baggage: "Optional[Baggage]" = None,
):
self._name: str = name
self._active: bool = active
Expand All @@ -91,8 +163,16 @@ def __init__(
for attribute, value in attributes.items():
self.set_attribute(attribute, value)

self._span_id: "Optional[str]" = None
self._scope = scope

self._segment = segment or self

self._trace_id: "Optional[str]" = trace_id
self._parent_span_id = parent_span_id
self._parent_sampled = parent_sampled
self._baggage = baggage

self._span_id: "Optional[str]" = None

self._status = SpanStatus.OK.value
self.set_attribute("sentry.span.source", SegmentSource.CUSTOM.value)
Expand All @@ -103,6 +183,7 @@ def __repr__(self) -> str:
f"name={self._name}, "
f"trace_id={self.trace_id}, "
f"span_id={self.span_id}, "
f"parent_span_id={self._parent_span_id}, "
f"active={self._active})>"
)

Expand Down Expand Up @@ -165,8 +246,18 @@ def trace_id(self) -> str:

return self._trace_id

@property
def sampled(self) -> "Optional[bool]":
return True


class NoOpStreamedSpan(StreamedSpan):
def __init__(self) -> None:
pass

def __repr__(self) -> str:
return f"<{self.__class__.__name__}(sampled={self.sampled})>"

def get_attributes(self) -> "Attributes":
return {}

Expand Down Expand Up @@ -206,3 +297,7 @@ def span_id(self) -> str:
@property
def trace_id(self) -> str:
return "00000000000000000000000000000000"

@property
def sampled(self) -> "Optional[bool]":
return False
Loading