traces.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  1. """
  2. EXPERIMENTAL. Do not use in production.
  3. The API in this file is only meant to be used in span streaming mode.
  4. You can enable span streaming mode via
  5. sentry_sdk.init(_experiments={"trace_lifecycle": "stream"}).
  6. """
  7. import uuid
  8. import warnings
  9. from datetime import datetime, timedelta, timezone
  10. from enum import Enum
  11. from typing import TYPE_CHECKING
  12. import sentry_sdk
  13. from sentry_sdk.consts import SPANDATA
  14. from sentry_sdk.profiler.continuous_profiler import (
  15. get_profiler_id,
  16. try_autostart_continuous_profiler,
  17. try_profile_lifecycle_trace_start,
  18. )
  19. from sentry_sdk.tracing_utils import Baggage
  20. from sentry_sdk.utils import (
  21. capture_internal_exceptions,
  22. format_attribute,
  23. get_current_thread_meta,
  24. logger,
  25. nanosecond_time,
  26. should_be_treated_as_error,
  27. )
  28. if TYPE_CHECKING:
  29. from typing import Any, Callable, Iterator, Optional, ParamSpec, TypeVar, Union
  30. from sentry_sdk._types import Attributes, AttributeValue
  31. from sentry_sdk.profiler.continuous_profiler import ContinuousProfile
  32. P = ParamSpec("P")
  33. R = TypeVar("R")
  34. BAGGAGE_HEADER_NAME = "baggage"
  35. SENTRY_TRACE_HEADER_NAME = "sentry-trace"
  36. class SpanStatus(str, Enum):
  37. OK = "ok"
  38. ERROR = "error"
  39. def __str__(self) -> str:
  40. return self.value
  41. # Segment source, see
  42. # https://getsentry.github.io/sentry-conventions/generated/attributes/sentry.html#sentryspansource
  43. class SegmentSource(str, Enum):
  44. COMPONENT = "component"
  45. CUSTOM = "custom"
  46. ROUTE = "route"
  47. TASK = "task"
  48. URL = "url"
  49. VIEW = "view"
  50. def __str__(self) -> str:
  51. return self.value
  52. # These are typically high cardinality and the server hates them
  53. LOW_QUALITY_SEGMENT_SOURCES = [
  54. SegmentSource.URL,
  55. ]
  56. SOURCE_FOR_STYLE = {
  57. "endpoint": SegmentSource.COMPONENT,
  58. "function_name": SegmentSource.COMPONENT,
  59. "handler_name": SegmentSource.COMPONENT,
  60. "method_and_path_pattern": SegmentSource.ROUTE,
  61. "path": SegmentSource.URL,
  62. "route_name": SegmentSource.COMPONENT,
  63. "route_pattern": SegmentSource.ROUTE,
  64. "uri_template": SegmentSource.ROUTE,
  65. "url": SegmentSource.ROUTE,
  66. }
  67. # Sentinel value for an unset parent_span to be able to distinguish it from
  68. # a None set by the user
  69. _DEFAULT_PARENT_SPAN = object()
  70. def start_span(
  71. name: str,
  72. attributes: "Optional[Attributes]" = None,
  73. parent_span: "Optional[StreamedSpan]" = _DEFAULT_PARENT_SPAN, # type: ignore[assignment]
  74. active: bool = True,
  75. ) -> "StreamedSpan":
  76. """
  77. Start a span.
  78. EXPERIMENTAL. Use sentry_sdk.start_transaction() and sentry_sdk.start_span()
  79. instead.
  80. The span's parent, unless provided explicitly via the `parent_span` argument,
  81. will be the current active span, if any. If there is none, this span will
  82. become the root of a new span tree. If you explicitly want this span to be
  83. top-level without a parent, set `parent_span=None`.
  84. `start_span()` can either be used as context manager or you can use the span
  85. object it returns and explicitly end it via `span.end()`. The following is
  86. equivalent:
  87. ```python
  88. import sentry_sdk
  89. with sentry_sdk.traces.start_span(name="My Span"):
  90. # do something
  91. # The span automatically finishes once the `with` block is exited
  92. ```
  93. ```python
  94. import sentry_sdk
  95. span = sentry_sdk.traces.start_span(name="My Span")
  96. # do something
  97. span.end()
  98. ```
  99. To continue a trace from another service, call
  100. `sentry_sdk.traces.continue_trace()` prior to creating a top-level span.
  101. :param name: The name to identify this span by.
  102. :type name: str
  103. :param attributes: Key-value attributes to set on the span from the start.
  104. These will also be accessible in the traces sampler.
  105. :type attributes: "Optional[Attributes]"
  106. :param parent_span: A span instance that the new span should consider its
  107. parent. If not provided, the parent will be set to the currently active
  108. span, if any. If set to `None`, this span will become a new root-level
  109. span.
  110. :type parent_span: "Optional[StreamedSpan]"
  111. :param active: Controls whether spans started while this span is running
  112. will automatically become its children. That's the default behavior. If
  113. you want to create a span that shouldn't have any children (unless
  114. provided explicitly via the `parent_span` argument), set this to `False`.
  115. :type active: bool
  116. :return: The span that has been started.
  117. :rtype: StreamedSpan
  118. """
  119. from sentry_sdk.tracing_utils import has_span_streaming_enabled
  120. if not has_span_streaming_enabled(sentry_sdk.get_client().options):
  121. warnings.warn(
  122. "Using span streaming API in non-span-streaming mode. Use "
  123. "sentry_sdk.start_transaction() and sentry_sdk.start_span() "
  124. "instead.",
  125. stacklevel=2,
  126. )
  127. return NoOpStreamedSpan()
  128. return sentry_sdk.get_current_scope().start_streamed_span(
  129. name, attributes, parent_span, active
  130. )
  131. def continue_trace(incoming: "dict[str, Any]") -> None:
  132. """
  133. Continue a trace from headers or environment variables.
  134. EXPERIMENTAL. Use sentry_sdk.continue_trace() instead.
  135. This function sets the propagation context on the scope. Any span started
  136. in the updated scope will belong under the trace extracted from the
  137. provided propagation headers or environment variables.
  138. continue_trace() doesn't start any spans on its own. Use the start_span()
  139. API for that.
  140. """
  141. # This is set both on the isolation and the current scope for compatibility
  142. # reasons. Conceptually, it belongs on the isolation scope, and it also
  143. # used to be set there in non-span-first mode. But in span first mode, we
  144. # start spans on the current scope, regardless of type, like JS does, so we
  145. # need to set the propagation context there.
  146. sentry_sdk.get_isolation_scope().generate_propagation_context(
  147. incoming,
  148. )
  149. sentry_sdk.get_current_scope().generate_propagation_context(
  150. incoming,
  151. )
  152. def new_trace() -> None:
  153. """
  154. Resets the propagation context, forcing a new trace.
  155. EXPERIMENTAL.
  156. This function sets the propagation context on the scope. Any span started
  157. in the updated scope will start its own trace.
  158. new_trace() doesn't start any spans on its own. Use the start_span() API
  159. for that.
  160. """
  161. sentry_sdk.get_isolation_scope().set_new_propagation_context()
  162. sentry_sdk.get_current_scope().set_new_propagation_context()
  163. class StreamedSpan:
  164. """
  165. A span holds timing information of a block of code.
  166. Spans can have multiple child spans, thus forming a span tree.
  167. This is the Span First span implementation that streams spans. The original
  168. transaction-based span implementation lives in tracing.Span.
  169. """
  170. __slots__ = (
  171. "_name",
  172. "_attributes",
  173. "_active",
  174. "_span_id",
  175. "_trace_id",
  176. "_parent_span_id",
  177. "_segment",
  178. "_parent_sampled",
  179. "_start_timestamp",
  180. "_start_timestamp_monotonic_ns",
  181. "_timestamp",
  182. "_status",
  183. "_scope",
  184. "_previous_span_on_scope",
  185. "_baggage",
  186. "_sample_rand",
  187. "_sample_rate",
  188. "_continuous_profile",
  189. )
  190. def __init__(
  191. self,
  192. *,
  193. name: str,
  194. attributes: "Optional[Attributes]" = None,
  195. active: bool = True,
  196. scope: "sentry_sdk.Scope",
  197. segment: "Optional[StreamedSpan]" = None,
  198. trace_id: "Optional[str]" = None,
  199. parent_span_id: "Optional[str]" = None,
  200. parent_sampled: "Optional[bool]" = None,
  201. baggage: "Optional[Baggage]" = None,
  202. sample_rate: "Optional[float]" = None,
  203. sample_rand: "Optional[float]" = None,
  204. ):
  205. self._name: str = name
  206. self._active: bool = active
  207. self._attributes: "Attributes" = {}
  208. if attributes:
  209. for attribute, value in attributes.items():
  210. self.set_attribute(attribute, value)
  211. self._scope = scope
  212. self._segment = segment or self
  213. self._trace_id: "Optional[str]" = trace_id
  214. self._parent_span_id = parent_span_id
  215. self._parent_sampled = parent_sampled
  216. self._baggage = baggage
  217. self._sample_rand = sample_rand
  218. self._sample_rate = sample_rate
  219. self._start_timestamp = datetime.now(timezone.utc)
  220. self._timestamp: "Optional[datetime]" = None
  221. try:
  222. # profiling depends on this value and requires that
  223. # it is measured in nanoseconds
  224. self._start_timestamp_monotonic_ns = nanosecond_time()
  225. except AttributeError:
  226. pass
  227. self._span_id: "Optional[str]" = None
  228. self._status = SpanStatus.OK.value
  229. self._update_active_thread()
  230. self._continuous_profile: "Optional[ContinuousProfile]" = None
  231. self._start_profile()
  232. self._set_profile_id(get_profiler_id())
  233. self._start()
  234. def __repr__(self) -> str:
  235. return (
  236. f"<{self.__class__.__name__}("
  237. f"name={self._name}, "
  238. f"trace_id={self.trace_id}, "
  239. f"span_id={self.span_id}, "
  240. f"parent_span_id={self._parent_span_id}, "
  241. f"active={self._active})>"
  242. )
  243. def __enter__(self) -> "StreamedSpan":
  244. return self
  245. def __exit__(
  246. self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
  247. ) -> None:
  248. if self._timestamp is not None:
  249. # This span is already finished, ignore
  250. return
  251. if value is not None and should_be_treated_as_error(ty, value):
  252. self.status = SpanStatus.ERROR.value
  253. self._end()
  254. def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
  255. """
  256. Finish this span and queue it for sending.
  257. :param end_timestamp: End timestamp to use instead of current time.
  258. :type end_timestamp: "Optional[Union[float, datetime]]"
  259. """
  260. self._end(end_timestamp)
  261. def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
  262. warnings.warn(
  263. "span.finish() is deprecated. Use span.end() instead.",
  264. stacklevel=2,
  265. category=DeprecationWarning,
  266. )
  267. self.end(end_timestamp)
  268. def _start(self) -> None:
  269. if self._active:
  270. old_span = self._scope.span
  271. self._scope.span = self
  272. self._previous_span_on_scope = old_span
  273. def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
  274. if self._timestamp is not None:
  275. # This span is already finished, ignore.
  276. return
  277. # Stop the profiler
  278. if self._is_segment() and self._continuous_profile is not None:
  279. with capture_internal_exceptions():
  280. self._continuous_profile.stop()
  281. # Detach from scope
  282. if self._active:
  283. with capture_internal_exceptions():
  284. old_span = self._previous_span_on_scope
  285. del self._previous_span_on_scope
  286. self._scope.span = old_span
  287. # Set attributes from the segment. These are set on span end on purpose
  288. # so that we have the best chance to capture the segment's final name
  289. # (since it might change during its lifetime)
  290. self.set_attribute("sentry.segment.id", self._segment.span_id)
  291. self.set_attribute("sentry.segment.name", self._segment.name)
  292. # Set the end timestamp
  293. if end_timestamp is not None:
  294. if isinstance(end_timestamp, (float, int)):
  295. try:
  296. end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc)
  297. except Exception:
  298. pass
  299. if isinstance(end_timestamp, datetime):
  300. self._timestamp = end_timestamp
  301. else:
  302. logger.debug(
  303. "[Tracing] Failed to set end_timestamp. Using current time instead."
  304. )
  305. if self._timestamp is None:
  306. try:
  307. elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns
  308. self._timestamp = self._start_timestamp + timedelta(
  309. microseconds=elapsed / 1000
  310. )
  311. except AttributeError:
  312. self._timestamp = datetime.now(timezone.utc)
  313. client = sentry_sdk.get_client()
  314. if not client.is_active():
  315. return
  316. # Finally, queue the span for sending to Sentry
  317. self._scope._capture_span(self)
  318. def get_attributes(self) -> "Attributes":
  319. return self._attributes
  320. def set_attribute(self, key: str, value: "AttributeValue") -> None:
  321. self._attributes[key] = format_attribute(value)
  322. def set_attributes(self, attributes: "Attributes") -> None:
  323. for key, value in attributes.items():
  324. self.set_attribute(key, value)
  325. def remove_attribute(self, key: str) -> None:
  326. try:
  327. del self._attributes[key]
  328. except KeyError:
  329. pass
  330. @property
  331. def status(self) -> "str":
  332. return self._status
  333. @status.setter
  334. def status(self, status: "Union[SpanStatus, str]") -> None:
  335. if isinstance(status, Enum):
  336. status = status.value
  337. if status not in {e.value for e in SpanStatus}:
  338. logger.debug(
  339. f'[Tracing] Unsupported span status {status}. Expected one of: "ok", "error"'
  340. )
  341. return
  342. self._status = status
  343. @property
  344. def name(self) -> str:
  345. return self._name
  346. @name.setter
  347. def name(self, name: str) -> None:
  348. self._name = name
  349. @property
  350. def active(self) -> bool:
  351. return self._active
  352. @property
  353. def span_id(self) -> str:
  354. if not self._span_id:
  355. self._span_id = uuid.uuid4().hex[16:]
  356. return self._span_id
  357. @property
  358. def trace_id(self) -> str:
  359. if not self._trace_id:
  360. self._trace_id = uuid.uuid4().hex
  361. return self._trace_id
  362. @property
  363. def sampled(self) -> "Optional[bool]":
  364. return True
  365. @property
  366. def start_timestamp(self) -> "Optional[datetime]":
  367. return self._start_timestamp
  368. @property
  369. def timestamp(self) -> "Optional[datetime]":
  370. return self._timestamp
  371. def _is_segment(self) -> bool:
  372. return self._segment is self
  373. def _update_active_thread(self) -> None:
  374. thread_id, thread_name = get_current_thread_meta()
  375. if thread_id is not None:
  376. self.set_attribute(SPANDATA.THREAD_ID, str(thread_id))
  377. if thread_name is not None:
  378. self.set_attribute(SPANDATA.THREAD_NAME, thread_name)
  379. def _dynamic_sampling_context(self) -> "dict[str, str]":
  380. return self._segment._get_baggage().dynamic_sampling_context()
  381. def _to_traceparent(self) -> str:
  382. if self.sampled is True:
  383. sampled = "1"
  384. elif self.sampled is False:
  385. sampled = "0"
  386. else:
  387. sampled = None
  388. traceparent = "%s-%s" % (self.trace_id, self.span_id)
  389. if sampled is not None:
  390. traceparent += "-%s" % (sampled,)
  391. return traceparent
  392. def _to_baggage(self) -> "Optional[Baggage]":
  393. if self._segment:
  394. return self._segment._get_baggage()
  395. return None
  396. def _get_baggage(self) -> "Baggage":
  397. """
  398. Return the :py:class:`~sentry_sdk.tracing_utils.Baggage` associated with
  399. the segment.
  400. The first time a new baggage with Sentry items is made, it will be frozen.
  401. """
  402. if not self._baggage or self._baggage.mutable:
  403. self._baggage = Baggage.populate_from_segment(self)
  404. return self._baggage
  405. def _iter_headers(self) -> "Iterator[tuple[str, str]]":
  406. if not self._segment:
  407. return
  408. yield SENTRY_TRACE_HEADER_NAME, self._to_traceparent()
  409. baggage = self._segment._get_baggage().serialize()
  410. if baggage:
  411. yield BAGGAGE_HEADER_NAME, baggage
  412. def _get_trace_context(self) -> "dict[str, Any]":
  413. # Even if spans themselves are not event-based anymore, we need this
  414. # to populate trace context on events
  415. context: "dict[str, Any]" = {
  416. "trace_id": self.trace_id,
  417. "span_id": self.span_id,
  418. "parent_span_id": self._parent_span_id,
  419. "dynamic_sampling_context": self._dynamic_sampling_context(),
  420. }
  421. if "sentry.op" in self._attributes:
  422. context["op"] = self._attributes["sentry.op"]
  423. if "sentry.origin" in self._attributes:
  424. context["origin"] = self._attributes["sentry.origin"]
  425. return context
  426. def _set_profile_id(self, profiler_id: "Optional[str]") -> None:
  427. if profiler_id is not None:
  428. self.set_attribute("sentry.profiler_id", profiler_id)
  429. def _start_profile(self) -> None:
  430. if not self._is_segment():
  431. return
  432. try_autostart_continuous_profiler()
  433. self._continuous_profile = try_profile_lifecycle_trace_start()
  434. class NoOpStreamedSpan(StreamedSpan):
  435. __slots__ = (
  436. "_finished",
  437. "_unsampled_reason",
  438. )
  439. def __init__(
  440. self,
  441. unsampled_reason: "Optional[str]" = None,
  442. scope: "Optional[sentry_sdk.Scope]" = None,
  443. ) -> None:
  444. self._scope = scope # type: ignore[assignment]
  445. self._unsampled_reason = unsampled_reason
  446. self._finished = False
  447. self._start()
  448. def __repr__(self) -> str:
  449. return f"<{self.__class__.__name__}(sampled={self.sampled})>"
  450. def __enter__(self) -> "NoOpStreamedSpan":
  451. return self
  452. def __exit__(
  453. self, ty: "Optional[Any]", value: "Optional[Any]", tb: "Optional[Any]"
  454. ) -> None:
  455. self._end()
  456. def _start(self) -> None:
  457. if self._scope is None:
  458. return
  459. old_span = self._scope.span
  460. self._scope.span = self
  461. self._previous_span_on_scope = old_span
  462. def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
  463. if self._finished:
  464. return
  465. if self._unsampled_reason is not None:
  466. client = sentry_sdk.get_client()
  467. if client.is_active() and client.transport:
  468. logger.debug(
  469. f"[Tracing] Discarding span because sampled=False (reason: {self._unsampled_reason})"
  470. )
  471. client.transport.record_lost_event(
  472. reason=self._unsampled_reason,
  473. data_category="span",
  474. quantity=1,
  475. )
  476. if self._scope and hasattr(self, "_previous_span_on_scope"):
  477. with capture_internal_exceptions():
  478. old_span = self._previous_span_on_scope
  479. del self._previous_span_on_scope
  480. self._scope.span = old_span
  481. self._finished = True
  482. def end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
  483. self._end()
  484. def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
  485. warnings.warn(
  486. "span.finish() is deprecated. Use span.end() instead.",
  487. stacklevel=2,
  488. category=DeprecationWarning,
  489. )
  490. self._end()
  491. def get_attributes(self) -> "Attributes":
  492. return {}
  493. def set_attribute(self, key: str, value: "AttributeValue") -> None:
  494. pass
  495. def set_attributes(self, attributes: "Attributes") -> None:
  496. pass
  497. def remove_attribute(self, key: str) -> None:
  498. pass
  499. def _is_segment(self) -> bool:
  500. return self._scope is not None
  501. @property
  502. def status(self) -> "str":
  503. return SpanStatus.OK.value
  504. @status.setter
  505. def status(self, status: "Union[SpanStatus, str]") -> None:
  506. pass
  507. @property
  508. def name(self) -> str:
  509. return ""
  510. @name.setter
  511. def name(self, value: str) -> None:
  512. pass
  513. @property
  514. def active(self) -> bool:
  515. return True
  516. @property
  517. def span_id(self) -> str:
  518. return "0000000000000000"
  519. @property
  520. def trace_id(self) -> str:
  521. return "00000000000000000000000000000000"
  522. @property
  523. def sampled(self) -> "Optional[bool]":
  524. return False
  525. @property
  526. def start_timestamp(self) -> "Optional[datetime]":
  527. return None
  528. @property
  529. def timestamp(self) -> "Optional[datetime]":
  530. return None
  531. def trace(
  532. func: "Optional[Callable[P, R]]" = None,
  533. *,
  534. name: "Optional[str]" = None,
  535. attributes: "Optional[dict[str, Any]]" = None,
  536. active: bool = True,
  537. ) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]":
  538. """
  539. Decorator to start a span around a function call.
  540. EXPERIMENTAL. Use @sentry_sdk.trace instead.
  541. This decorator automatically creates a new span when the decorated function
  542. is called, and finishes the span when the function returns or raises an exception.
  543. :param func: The function to trace. When used as a decorator without parentheses,
  544. this is the function being decorated. When used with parameters (e.g.,
  545. ``@trace(op="custom")``, this should be None.
  546. :type func: Callable or None
  547. :param name: The human-readable name/description for the span. If not provided,
  548. defaults to the function name. This provides more specific details about
  549. what the span represents (e.g., "GET /api/users", "process_user_data").
  550. :type name: str or None
  551. :param attributes: A dictionary of key-value pairs to add as attributes to the span.
  552. Attribute values must be strings, integers, floats, or booleans. These
  553. attributes provide additional context about the span's execution.
  554. :type attributes: dict[str, Any] or None
  555. :param active: Controls whether spans started while this span is running
  556. will automatically become its children. That's the default behavior. If
  557. you want to create a span that shouldn't have any children (unless
  558. provided explicitly via the `parent_span` argument), set this to False.
  559. :type active: bool
  560. :returns: When used as ``@trace``, returns the decorated function. When used as
  561. ``@trace(...)`` with parameters, returns a decorator function.
  562. :rtype: Callable or decorator function
  563. Example::
  564. import sentry_sdk
  565. # Simple usage with default values
  566. @sentry_sdk.trace
  567. def process_data():
  568. # Function implementation
  569. pass
  570. # With custom parameters
  571. @sentry_sdk.trace(
  572. name="Get user data",
  573. attributes={"postgres": True}
  574. )
  575. def make_db_query(sql):
  576. # Function implementation
  577. pass
  578. """
  579. from sentry_sdk.tracing_utils import (
  580. create_streaming_span_decorator,
  581. )
  582. decorator = create_streaming_span_decorator(
  583. name=name,
  584. attributes=attributes,
  585. active=active,
  586. )
  587. if func:
  588. return decorator(func)
  589. else:
  590. return decorator