Skip to content
Open
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
55 changes: 54 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 _DEFAULT_PARENT_SPAN, StreamedSpan, NoOpStreamedSpan
from sentry_sdk.tracing import (
BAGGAGE_HEADER_NAME,
SENTRY_TRACE_HEADER_NAME,
Expand Down Expand Up @@ -1174,6 +1174,59 @@ def start_span(

return span

def start_streamed_span(
self,
name: str,
attributes: "Optional[Attributes]",
parent_span: "Optional[StreamedSpan]",
active: bool,
) -> "StreamedSpan":
Comment on lines +1177 to +1183
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any way to explicitly create a span with is_segment=True?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is and this PR doesn't implement it correctly. Will fix.

The supported way to do that is to explicitly provide start_span(parent_span=None). The way it's implemented now in this PR, we don't distinguish between a user-set parent_span=None and the fallback value of None, which will actually try to parent the span to the currently active span. We'll need a different sentinel value for parent_span so that we can tell it apart from parent_span=None.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

# 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 _DEFAULT_PARENT_SPAN 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()

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
104 changes: 103 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,73 @@ def __str__(self) -> str:
}


# Sentinel value for an unset parent_span to be able to distinguish it from
# a None set by the user
_DEFAULT_PARENT_SPAN = object()


def start_span(
name: str,
attributes: "Optional[Attributes]" = None,
parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment]
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. If you explicitly want this span to be
top-level without a parent, set `parent_span=None`.

`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. If set to `None`, this span will become a new root-level
span.
: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 +142,12 @@ class StreamedSpan:
"_active",
"_span_id",
"_trace_id",
"_parent_span_id",
"_segment",
"_parent_sampled",
"_status",
"_scope",
"_baggage",
)

def __init__(
Expand All @@ -82,7 +156,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 +170,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 +190,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 +253,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 +304,7 @@ def span_id(self) -> str:
@property
def trace_id(self) -> str:
return "00000000000000000000000000000000"

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