Skip to content

Commit b3f4f98

Browse files
committed
Cant use sentry_sdk.trace as that already exists
1 parent 705790a commit b3f4f98

File tree

5 files changed

+417
-2
lines changed

5 files changed

+417
-2
lines changed
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
from sentry_sdk.consts import SPANDATA
88
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
99
from sentry_sdk.tracing import Span
10-
from sentry_sdk.tracing_utils import has_span_streaming_enabled, has_tracing_enabled
10+
from sentry_sdk.tracing_utils import (
11+
Baggage,
12+
has_span_streaming_enabled,
13+
has_tracing_enabled,
14+
)
1115
from sentry_sdk.utils import (
1216
capture_internal_exceptions,
1317
format_attribute,
@@ -120,6 +124,7 @@ class StreamedSpan:
120124
"_context_manager_state",
121125
"_profile",
122126
"_continuous_profile",
127+
"_baggage",
123128
)
124129

125130
def __init__(
@@ -303,6 +308,11 @@ def trace_id(self) -> str:
303308

304309
return self._trace_id
305310

311+
@property
312+
def dynamic_sampling_context(self) -> str:
313+
# TODO
314+
return self.segment.get_baggage().dynamic_sampling_context()
315+
306316
def _update_active_thread(self) -> None:
307317
thread_id, thread_name = get_current_thread_meta()
308318
self._set_thread(thread_id, thread_name)

sentry_sdk/_tracing.py

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
import uuid
2+
from datetime import datetime, timedelta, timezone
3+
from enum import Enum
4+
from typing import TYPE_CHECKING
5+
6+
import sentry_sdk
7+
from sentry_sdk.consts import SPANDATA
8+
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
9+
from sentry_sdk.tracing_utils import (
10+
Baggage,
11+
has_span_streaming_enabled,
12+
has_tracing_enabled,
13+
)
14+
from sentry_sdk.utils import (
15+
capture_internal_exceptions,
16+
format_attribute,
17+
get_current_thread_meta,
18+
logger,
19+
nanosecond_time,
20+
should_be_treated_as_error,
21+
)
22+
23+
if TYPE_CHECKING:
24+
from typing import Any, Optional, Union
25+
from sentry_sdk._types import Attributes, AttributeValue
26+
from sentry_sdk.scope import Scope
27+
28+
29+
FLAGS_CAPACITY = 10
30+
31+
"""
32+
TODO[span-first] / notes
33+
- redis, http, subprocess breadcrumbs (maybe_create_breadcrumbs_from_span) work
34+
on op, change or ignore?
35+
- @trace
36+
- tags
37+
- initial status: OK? or unset?
38+
- dropped spans are not migrated
39+
- recheck transaction.finish <-> Streamedspan.end
40+
- profile not part of the event, how to send?
41+
- maybe: use getters/setter OR properties but not both
42+
- add size-based flushing to buffer(s)
43+
- migrate transaction sample_rand logic
44+
45+
Notes:
46+
- removed ability to provide a start_timestamp
47+
- moved _flags_capacity to a const
48+
"""
49+
50+
51+
def start_span(
52+
name: str,
53+
attributes: "Optional[Attributes]" = None,
54+
parent_span: "Optional[StreamedSpan]" = None,
55+
) -> "StreamedSpan":
56+
return sentry_sdk.get_current_scope().start_streamed_span()
57+
58+
59+
class SpanStatus(str, Enum):
60+
OK = "ok"
61+
ERROR = "error"
62+
63+
def __str__(self) -> str:
64+
return self.value
65+
66+
67+
# Segment source, see
68+
# https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource
69+
class SegmentSource(str, Enum):
70+
COMPONENT = "component"
71+
CUSTOM = "custom"
72+
ROUTE = "route"
73+
TASK = "task"
74+
URL = "url"
75+
VIEW = "view"
76+
77+
def __str__(self) -> str:
78+
return self.value
79+
80+
81+
# These are typically high cardinality and the server hates them
82+
LOW_QUALITY_SEGMENT_SOURCES = [
83+
SegmentSource.URL,
84+
]
85+
86+
87+
SOURCE_FOR_STYLE = {
88+
"endpoint": SegmentSource.COMPONENT,
89+
"function_name": SegmentSource.COMPONENT,
90+
"handler_name": SegmentSource.COMPONENT,
91+
"method_and_path_pattern": SegmentSource.ROUTE,
92+
"path": SegmentSource.URL,
93+
"route_name": SegmentSource.COMPONENT,
94+
"route_pattern": SegmentSource.ROUTE,
95+
"uri_template": SegmentSource.ROUTE,
96+
"url": SegmentSource.ROUTE,
97+
}
98+
99+
100+
class StreamedSpan:
101+
"""
102+
A span holds timing information of a block of code.
103+
104+
Spans can have multiple child spans thus forming a span tree.
105+
106+
This is the Span First span implementation. The original transaction-based
107+
span implementation lives in tracing.Span.
108+
"""
109+
110+
__slots__ = (
111+
"_name",
112+
"_attributes",
113+
"_span_id",
114+
"_trace_id",
115+
"_parent_span_id",
116+
"_segment",
117+
"_sampled",
118+
"_start_timestamp",
119+
"_timestamp",
120+
"_status",
121+
"_start_timestamp_monotonic_ns",
122+
"_scope",
123+
"_flags",
124+
"_context_manager_state",
125+
"_profile",
126+
"_continuous_profile",
127+
"_baggage",
128+
"_sample_rate",
129+
"_sample_rand",
130+
)
131+
132+
def __init__(
133+
self,
134+
name: str,
135+
trace_id: str,
136+
attributes: "Optional[Attributes]" = None,
137+
parent_span_id: "Optional[str]" = None,
138+
segment: "Optional[StreamedSpan]" = None,
139+
scope: "Optional[Scope]" = None,
140+
) -> None:
141+
self._name: str = name
142+
self._attributes: "Attributes" = attributes
143+
144+
self._trace_id = trace_id
145+
self._parent_span_id = parent_span_id
146+
self._segment = segment or self
147+
148+
self._start_timestamp = datetime.now(timezone.utc)
149+
150+
try:
151+
# profiling depends on this value and requires that
152+
# it is measured in nanoseconds
153+
self._start_timestamp_monotonic_ns = nanosecond_time()
154+
except AttributeError:
155+
pass
156+
157+
self._timestamp: "Optional[datetime]" = None
158+
self._span_id: "Optional[str]" = None
159+
self._status: SpanStatus = SpanStatus.OK
160+
self._sampled: "Optional[bool]" = None
161+
self._scope: "Optional[Scope]" = scope # TODO[span-first] when are we starting a span with a specific scope? is this needed?
162+
self._flags: dict[str, bool] = {}
163+
164+
self._update_active_thread()
165+
self._set_profiler_id(get_profiler_id())
166+
167+
def __repr__(self) -> str:
168+
return (
169+
f"<{self.__class__.__name__}("
170+
f"name={self._name}, "
171+
f"trace_id={self._trace_id}, "
172+
f"span_id={self._span_id}, "
173+
f"parent_span_id={self._parent_span_id}, "
174+
f"sampled={self._sampled})>"
175+
)
176+
177+
def __enter__(self) -> "StreamedSpan":
178+
scope = self._scope or sentry_sdk.get_current_scope()
179+
old_span = scope.span
180+
scope.span = self
181+
self._context_manager_state = (scope, old_span)
182+
183+
if self.is_segment() and self._profile is not None:
184+
self._profile.__enter__()
185+
186+
return self
187+
188+
def __exit__(
189+
self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
190+
) -> None:
191+
if self.is_segment():
192+
if self._profile is not None:
193+
self._profile.__exit__(ty, value, tb)
194+
195+
if self._continuous_profile is not None:
196+
self._continuous_profile.stop()
197+
198+
if value is not None and should_be_treated_as_error(ty, value):
199+
self.set_status(SpanStatus.ERROR)
200+
201+
with capture_internal_exceptions():
202+
scope, old_span = self._context_manager_state
203+
del self._context_manager_state
204+
self.end(scope=scope)
205+
scope.span = old_span
206+
207+
def end(
208+
self,
209+
end_timestamp: "Optional[Union[float, datetime]]" = None,
210+
scope: "Optional[sentry_sdk.Scope]" = None,
211+
) -> "Optional[str]":
212+
"""
213+
Set the end timestamp of the span.
214+
215+
:param end_timestamp: Optional timestamp that should
216+
be used as timestamp instead of the current time.
217+
:param scope: The scope to use for this transaction.
218+
If not provided, the current scope will be used.
219+
"""
220+
client = sentry_sdk.get_client()
221+
if not client.is_active():
222+
return None
223+
224+
scope: "Optional[sentry_sdk.Scope]" = (
225+
scope or self._scope or sentry_sdk.get_current_scope()
226+
)
227+
228+
# Explicit check against False needed because self.sampled might be None
229+
if self._sampled is False:
230+
logger.debug("Discarding span because sampled = False")
231+
232+
# This is not entirely accurate because discards here are not
233+
# exclusively based on sample rate but also traces sampler, but
234+
# we handle this the same here.
235+
if client.transport and has_tracing_enabled(client.options):
236+
if client.monitor and client.monitor.downsample_factor > 0:
237+
reason = "backpressure"
238+
else:
239+
reason = "sample_rate"
240+
241+
client.transport.record_lost_event(reason, data_category="span")
242+
243+
return None
244+
245+
if self._sampled is None:
246+
logger.warning("Discarding transaction without sampling decision.")
247+
248+
if self.timestamp is not None:
249+
# This span is already finished, ignore.
250+
return None
251+
252+
try:
253+
if end_timestamp:
254+
if isinstance(end_timestamp, float):
255+
end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc)
256+
self.timestamp = end_timestamp
257+
else:
258+
elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns
259+
self.timestamp = self._start_timestamp + timedelta(
260+
microseconds=elapsed / 1000
261+
)
262+
except AttributeError:
263+
self.timestamp = datetime.now(timezone.utc)
264+
265+
if self.segment.sampled:
266+
client._capture_span(self)
267+
return
268+
269+
def get_attributes(self) -> "Attributes":
270+
return self._attributes
271+
272+
def set_attribute(self, key: str, value: "AttributeValue") -> None:
273+
self._attributes[key] = format_attribute(value)
274+
275+
def set_attributes(self, attributes: "Attributes") -> None:
276+
for key, value in attributes.items():
277+
self.set_attribute(key, value)
278+
279+
def set_status(self, status: SpanStatus) -> None:
280+
self._status = status
281+
282+
def get_name(self) -> str:
283+
return self._name
284+
285+
def set_name(self, name: str) -> None:
286+
self._name = name
287+
288+
@property
289+
def segment(self) -> "StreamedSpan":
290+
return self._segment
291+
292+
def is_segment(self) -> bool:
293+
return self.segment == self
294+
295+
@property
296+
def sampled(self) -> "Optional[bool]":
297+
return self._sampled
298+
299+
@property
300+
def span_id(self) -> str:
301+
if not self._span_id:
302+
self._span_id = uuid.uuid4().hex[16:]
303+
304+
return self._span_id
305+
306+
@property
307+
def trace_id(self) -> str:
308+
if not self._trace_id:
309+
self._trace_id = uuid.uuid4().hex
310+
311+
return self._trace_id
312+
313+
@property
314+
def dynamic_sampling_context(self) -> str:
315+
# TODO
316+
return self.segment.get_baggage().dynamic_sampling_context()
317+
318+
def _update_active_thread(self) -> None:
319+
thread_id, thread_name = get_current_thread_meta()
320+
self._set_thread(thread_id, thread_name)
321+
322+
def _set_thread(
323+
self, thread_id: "Optional[int]", thread_name: "Optional[str]"
324+
) -> None:
325+
if thread_id is not None:
326+
self.set_attribute(SPANDATA.THREAD_ID, str(thread_id))
327+
328+
if thread_name is not None:
329+
self.set_attribute(SPANDATA.THREAD_NAME, thread_name)
330+
331+
def _set_profiler_id(self, profiler_id: "Optional[str]") -> None:
332+
if profiler_id is not None:
333+
self.set_attribute(SPANDATA.PROFILER_ID, profiler_id)
334+
335+
def _set_http_status(self, http_status: int) -> None:
336+
self.set_attribute(SPANDATA.HTTP_STATUS_CODE, http_status)
337+
338+
if http_status >= 400:
339+
self.set_status(SpanStatus.ERROR)
340+
else:
341+
self.set_status(SpanStatus.OK)
342+
343+
def _get_baggage(self) -> "Baggage":
344+
"""
345+
Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with
346+
the segment.
347+
348+
The first time a new baggage with Sentry items is made, it will be frozen.
349+
"""
350+
if not self._baggage or self._baggage.mutable:
351+
self._baggage = Baggage.populate_from_segment(self)
352+
353+
return self._baggage

sentry_sdk/scope.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
normalize_incoming_data,
3333
PropagationContext,
3434
)
35-
from sentry_sdk.trace import StreamedSpan
35+
from sentry_sdk._tracing import StreamedSpan
3636
from sentry_sdk.tracing import (
3737
BAGGAGE_HEADER_NAME,
3838
SENTRY_TRACE_HEADER_NAME,

0 commit comments

Comments
 (0)