您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

1389 行
47 KiB

  1. from decimal import Decimal
  2. import uuid
  3. import warnings
  4. from datetime import datetime, timedelta, timezone
  5. from enum import Enum
  6. import sentry_sdk
  7. from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA
  8. from sentry_sdk.profiler.continuous_profiler import get_profiler_id
  9. from sentry_sdk.utils import (
  10. get_current_thread_meta,
  11. is_valid_sample_rate,
  12. logger,
  13. nanosecond_time,
  14. should_be_treated_as_error,
  15. )
  16. from typing import TYPE_CHECKING
  17. if TYPE_CHECKING:
  18. from collections.abc import Callable, Mapping, MutableMapping
  19. from typing import Any
  20. from typing import Dict
  21. from typing import Iterator
  22. from typing import List
  23. from typing import Optional
  24. from typing import overload
  25. from typing import ParamSpec
  26. from typing import Tuple
  27. from typing import Union
  28. from typing import TypeVar
  29. from typing_extensions import TypedDict, Unpack
  30. P = ParamSpec("P")
  31. R = TypeVar("R")
  32. from sentry_sdk.profiler.continuous_profiler import ContinuousProfile
  33. from sentry_sdk.profiler.transaction_profiler import Profile
  34. from sentry_sdk._types import (
  35. Event,
  36. MeasurementUnit,
  37. SamplingContext,
  38. MeasurementValue,
  39. )
  40. class SpanKwargs(TypedDict, total=False):
  41. trace_id: str
  42. """
  43. The trace ID of the root span. If this new span is to be the root span,
  44. omit this parameter, and a new trace ID will be generated.
  45. """
  46. span_id: str
  47. """The span ID of this span. If omitted, a new span ID will be generated."""
  48. parent_span_id: str
  49. """The span ID of the parent span, if applicable."""
  50. same_process_as_parent: bool
  51. """Whether this span is in the same process as the parent span."""
  52. sampled: bool
  53. """
  54. Whether the span should be sampled. Overrides the default sampling decision
  55. for this span when provided.
  56. """
  57. op: str
  58. """
  59. The span's operation. A list of recommended values is available here:
  60. https://develop.sentry.dev/sdk/performance/span-operations/
  61. """
  62. description: str
  63. """A description of what operation is being performed within the span. This argument is DEPRECATED. Please use the `name` parameter, instead."""
  64. hub: Optional["sentry_sdk.Hub"]
  65. """The hub to use for this span. This argument is DEPRECATED. Please use the `scope` parameter, instead."""
  66. status: str
  67. """The span's status. Possible values are listed at https://develop.sentry.dev/sdk/event-payloads/span/"""
  68. containing_transaction: Optional["Transaction"]
  69. """The transaction that this span belongs to."""
  70. start_timestamp: Optional[Union[datetime, float]]
  71. """
  72. The timestamp when the span started. If omitted, the current time
  73. will be used.
  74. """
  75. scope: "sentry_sdk.Scope"
  76. """The scope to use for this span. If not provided, we use the current scope."""
  77. origin: str
  78. """
  79. The origin of the span.
  80. See https://develop.sentry.dev/sdk/performance/trace-origin/
  81. Default "manual".
  82. """
  83. name: str
  84. """A string describing what operation is being performed within the span/transaction."""
  85. class TransactionKwargs(SpanKwargs, total=False):
  86. source: str
  87. """
  88. A string describing the source of the transaction name. This will be used to determine the transaction's type.
  89. See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations for more information.
  90. Default "custom".
  91. """
  92. parent_sampled: bool
  93. """Whether the parent transaction was sampled. If True this transaction will be kept, if False it will be discarded."""
  94. baggage: "Baggage"
  95. """The W3C baggage header value. (see https://www.w3.org/TR/baggage/)"""
  96. ProfileContext = TypedDict(
  97. "ProfileContext",
  98. {
  99. "profiler_id": str,
  100. },
  101. )
  102. BAGGAGE_HEADER_NAME = "baggage"
  103. SENTRY_TRACE_HEADER_NAME = "sentry-trace"
  104. # Transaction source
  105. # see https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
  106. class TransactionSource(str, Enum):
  107. COMPONENT = "component"
  108. CUSTOM = "custom"
  109. ROUTE = "route"
  110. TASK = "task"
  111. URL = "url"
  112. VIEW = "view"
  113. def __str__(self):
  114. # type: () -> str
  115. return self.value
  116. # These are typically high cardinality and the server hates them
  117. LOW_QUALITY_TRANSACTION_SOURCES = [
  118. TransactionSource.URL,
  119. ]
  120. SOURCE_FOR_STYLE = {
  121. "endpoint": TransactionSource.COMPONENT,
  122. "function_name": TransactionSource.COMPONENT,
  123. "handler_name": TransactionSource.COMPONENT,
  124. "method_and_path_pattern": TransactionSource.ROUTE,
  125. "path": TransactionSource.URL,
  126. "route_name": TransactionSource.COMPONENT,
  127. "route_pattern": TransactionSource.ROUTE,
  128. "uri_template": TransactionSource.ROUTE,
  129. "url": TransactionSource.ROUTE,
  130. }
  131. def get_span_status_from_http_code(http_status_code):
  132. # type: (int) -> str
  133. """
  134. Returns the Sentry status corresponding to the given HTTP status code.
  135. See: https://develop.sentry.dev/sdk/event-payloads/contexts/#trace-context
  136. """
  137. if http_status_code < 400:
  138. return SPANSTATUS.OK
  139. elif 400 <= http_status_code < 500:
  140. if http_status_code == 403:
  141. return SPANSTATUS.PERMISSION_DENIED
  142. elif http_status_code == 404:
  143. return SPANSTATUS.NOT_FOUND
  144. elif http_status_code == 429:
  145. return SPANSTATUS.RESOURCE_EXHAUSTED
  146. elif http_status_code == 413:
  147. return SPANSTATUS.FAILED_PRECONDITION
  148. elif http_status_code == 401:
  149. return SPANSTATUS.UNAUTHENTICATED
  150. elif http_status_code == 409:
  151. return SPANSTATUS.ALREADY_EXISTS
  152. else:
  153. return SPANSTATUS.INVALID_ARGUMENT
  154. elif 500 <= http_status_code < 600:
  155. if http_status_code == 504:
  156. return SPANSTATUS.DEADLINE_EXCEEDED
  157. elif http_status_code == 501:
  158. return SPANSTATUS.UNIMPLEMENTED
  159. elif http_status_code == 503:
  160. return SPANSTATUS.UNAVAILABLE
  161. else:
  162. return SPANSTATUS.INTERNAL_ERROR
  163. return SPANSTATUS.UNKNOWN_ERROR
  164. class _SpanRecorder:
  165. """Limits the number of spans recorded in a transaction."""
  166. __slots__ = ("maxlen", "spans", "dropped_spans")
  167. def __init__(self, maxlen):
  168. # type: (int) -> None
  169. # FIXME: this is `maxlen - 1` only to preserve historical behavior
  170. # enforced by tests.
  171. # Either this should be changed to `maxlen` or the JS SDK implementation
  172. # should be changed to match a consistent interpretation of what maxlen
  173. # limits: either transaction+spans or only child spans.
  174. self.maxlen = maxlen - 1
  175. self.spans = [] # type: List[Span]
  176. self.dropped_spans = 0 # type: int
  177. def add(self, span):
  178. # type: (Span) -> None
  179. if len(self.spans) > self.maxlen:
  180. span._span_recorder = None
  181. self.dropped_spans += 1
  182. else:
  183. self.spans.append(span)
  184. class Span:
  185. """A span holds timing information of a block of code.
  186. Spans can have multiple child spans thus forming a span tree.
  187. :param trace_id: The trace ID of the root span. If this new span is to be the root span,
  188. omit this parameter, and a new trace ID will be generated.
  189. :param span_id: The span ID of this span. If omitted, a new span ID will be generated.
  190. :param parent_span_id: The span ID of the parent span, if applicable.
  191. :param same_process_as_parent: Whether this span is in the same process as the parent span.
  192. :param sampled: Whether the span should be sampled. Overrides the default sampling decision
  193. for this span when provided.
  194. :param op: The span's operation. A list of recommended values is available here:
  195. https://develop.sentry.dev/sdk/performance/span-operations/
  196. :param description: A description of what operation is being performed within the span.
  197. .. deprecated:: 2.15.0
  198. Please use the `name` parameter, instead.
  199. :param name: A string describing what operation is being performed within the span.
  200. :param hub: The hub to use for this span.
  201. .. deprecated:: 2.0.0
  202. Please use the `scope` parameter, instead.
  203. :param status: The span's status. Possible values are listed at
  204. https://develop.sentry.dev/sdk/event-payloads/span/
  205. :param containing_transaction: The transaction that this span belongs to.
  206. :param start_timestamp: The timestamp when the span started. If omitted, the current time
  207. will be used.
  208. :param scope: The scope to use for this span. If not provided, we use the current scope.
  209. """
  210. __slots__ = (
  211. "trace_id",
  212. "span_id",
  213. "parent_span_id",
  214. "same_process_as_parent",
  215. "sampled",
  216. "op",
  217. "description",
  218. "_measurements",
  219. "start_timestamp",
  220. "_start_timestamp_monotonic_ns",
  221. "status",
  222. "timestamp",
  223. "_tags",
  224. "_data",
  225. "_span_recorder",
  226. "hub",
  227. "_context_manager_state",
  228. "_containing_transaction",
  229. "_local_aggregator",
  230. "scope",
  231. "origin",
  232. "name",
  233. "_flags",
  234. "_flags_capacity",
  235. )
  236. def __init__(
  237. self,
  238. trace_id=None, # type: Optional[str]
  239. span_id=None, # type: Optional[str]
  240. parent_span_id=None, # type: Optional[str]
  241. same_process_as_parent=True, # type: bool
  242. sampled=None, # type: Optional[bool]
  243. op=None, # type: Optional[str]
  244. description=None, # type: Optional[str]
  245. hub=None, # type: Optional[sentry_sdk.Hub] # deprecated
  246. status=None, # type: Optional[str]
  247. containing_transaction=None, # type: Optional[Transaction]
  248. start_timestamp=None, # type: Optional[Union[datetime, float]]
  249. scope=None, # type: Optional[sentry_sdk.Scope]
  250. origin="manual", # type: str
  251. name=None, # type: Optional[str]
  252. ):
  253. # type: (...) -> None
  254. self.trace_id = trace_id or uuid.uuid4().hex
  255. self.span_id = span_id or uuid.uuid4().hex[16:]
  256. self.parent_span_id = parent_span_id
  257. self.same_process_as_parent = same_process_as_parent
  258. self.sampled = sampled
  259. self.op = op
  260. self.description = name or description
  261. self.status = status
  262. self.hub = hub # backwards compatibility
  263. self.scope = scope
  264. self.origin = origin
  265. self._measurements = {} # type: Dict[str, MeasurementValue]
  266. self._tags = {} # type: MutableMapping[str, str]
  267. self._data = {} # type: Dict[str, Any]
  268. self._containing_transaction = containing_transaction
  269. self._flags = {} # type: Dict[str, bool]
  270. self._flags_capacity = 10
  271. if hub is not None:
  272. warnings.warn(
  273. "The `hub` parameter is deprecated. Please use `scope` instead.",
  274. DeprecationWarning,
  275. stacklevel=2,
  276. )
  277. self.scope = self.scope or hub.scope
  278. if start_timestamp is None:
  279. start_timestamp = datetime.now(timezone.utc)
  280. elif isinstance(start_timestamp, float):
  281. start_timestamp = datetime.fromtimestamp(start_timestamp, timezone.utc)
  282. self.start_timestamp = start_timestamp
  283. try:
  284. # profiling depends on this value and requires that
  285. # it is measured in nanoseconds
  286. self._start_timestamp_monotonic_ns = nanosecond_time()
  287. except AttributeError:
  288. pass
  289. #: End timestamp of span
  290. self.timestamp = None # type: Optional[datetime]
  291. self._span_recorder = None # type: Optional[_SpanRecorder]
  292. self._local_aggregator = None # type: Optional[LocalAggregator]
  293. self.update_active_thread()
  294. self.set_profiler_id(get_profiler_id())
  295. # TODO this should really live on the Transaction class rather than the Span
  296. # class
  297. def init_span_recorder(self, maxlen):
  298. # type: (int) -> None
  299. if self._span_recorder is None:
  300. self._span_recorder = _SpanRecorder(maxlen)
  301. def _get_local_aggregator(self):
  302. # type: (...) -> LocalAggregator
  303. rv = self._local_aggregator
  304. if rv is None:
  305. rv = self._local_aggregator = LocalAggregator()
  306. return rv
  307. def __repr__(self):
  308. # type: () -> str
  309. return (
  310. "<%s(op=%r, description:%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, origin=%r)>"
  311. % (
  312. self.__class__.__name__,
  313. self.op,
  314. self.description,
  315. self.trace_id,
  316. self.span_id,
  317. self.parent_span_id,
  318. self.sampled,
  319. self.origin,
  320. )
  321. )
  322. def __enter__(self):
  323. # type: () -> Span
  324. scope = self.scope or sentry_sdk.get_current_scope()
  325. old_span = scope.span
  326. scope.span = self
  327. self._context_manager_state = (scope, old_span)
  328. return self
  329. def __exit__(self, ty, value, tb):
  330. # type: (Optional[Any], Optional[Any], Optional[Any]) -> None
  331. if value is not None and should_be_treated_as_error(ty, value):
  332. self.set_status(SPANSTATUS.INTERNAL_ERROR)
  333. scope, old_span = self._context_manager_state
  334. del self._context_manager_state
  335. self.finish(scope)
  336. scope.span = old_span
  337. @property
  338. def containing_transaction(self):
  339. # type: () -> Optional[Transaction]
  340. """The ``Transaction`` that this span belongs to.
  341. The ``Transaction`` is the root of the span tree,
  342. so one could also think of this ``Transaction`` as the "root span"."""
  343. # this is a getter rather than a regular attribute so that transactions
  344. # can return `self` here instead (as a way to prevent them circularly
  345. # referencing themselves)
  346. return self._containing_transaction
  347. def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
  348. # type: (str, **Any) -> Span
  349. """
  350. Start a sub-span from the current span or transaction.
  351. Takes the same arguments as the initializer of :py:class:`Span`. The
  352. trace id, sampling decision, transaction pointer, and span recorder are
  353. inherited from the current span/transaction.
  354. The instrumenter parameter is deprecated for user code, and it will
  355. be removed in the next major version. Going forward, it should only
  356. be used by the SDK itself.
  357. """
  358. if kwargs.get("description") is not None:
  359. warnings.warn(
  360. "The `description` parameter is deprecated. Please use `name` instead.",
  361. DeprecationWarning,
  362. stacklevel=2,
  363. )
  364. configuration_instrumenter = sentry_sdk.get_client().options["instrumenter"]
  365. if instrumenter != configuration_instrumenter:
  366. return NoOpSpan()
  367. kwargs.setdefault("sampled", self.sampled)
  368. child = Span(
  369. trace_id=self.trace_id,
  370. parent_span_id=self.span_id,
  371. containing_transaction=self.containing_transaction,
  372. **kwargs,
  373. )
  374. span_recorder = (
  375. self.containing_transaction and self.containing_transaction._span_recorder
  376. )
  377. if span_recorder:
  378. span_recorder.add(child)
  379. return child
  380. @classmethod
  381. def continue_from_environ(
  382. cls,
  383. environ, # type: Mapping[str, str]
  384. **kwargs, # type: Any
  385. ):
  386. # type: (...) -> Transaction
  387. """
  388. Create a Transaction with the given params, then add in data pulled from
  389. the ``sentry-trace`` and ``baggage`` headers from the environ (if any)
  390. before returning the Transaction.
  391. This is different from :py:meth:`~sentry_sdk.tracing.Span.continue_from_headers`
  392. in that it assumes header names in the form ``HTTP_HEADER_NAME`` -
  393. such as you would get from a WSGI/ASGI environ -
  394. rather than the form ``header-name``.
  395. :param environ: The ASGI/WSGI environ to pull information from.
  396. """
  397. if cls is Span:
  398. logger.warning(
  399. "Deprecated: use Transaction.continue_from_environ "
  400. "instead of Span.continue_from_environ."
  401. )
  402. return Transaction.continue_from_headers(EnvironHeaders(environ), **kwargs)
  403. @classmethod
  404. def continue_from_headers(
  405. cls,
  406. headers, # type: Mapping[str, str]
  407. *,
  408. _sample_rand=None, # type: Optional[str]
  409. **kwargs, # type: Any
  410. ):
  411. # type: (...) -> Transaction
  412. """
  413. Create a transaction with the given params (including any data pulled from
  414. the ``sentry-trace`` and ``baggage`` headers).
  415. :param headers: The dictionary with the HTTP headers to pull information from.
  416. :param _sample_rand: If provided, we override the sample_rand value from the
  417. incoming headers with this value. (internal use only)
  418. """
  419. # TODO move this to the Transaction class
  420. if cls is Span:
  421. logger.warning(
  422. "Deprecated: use Transaction.continue_from_headers "
  423. "instead of Span.continue_from_headers."
  424. )
  425. # TODO-neel move away from this kwargs stuff, it's confusing and opaque
  426. # make more explicit
  427. baggage = Baggage.from_incoming_header(
  428. headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand
  429. )
  430. kwargs.update({BAGGAGE_HEADER_NAME: baggage})
  431. sentrytrace_kwargs = extract_sentrytrace_data(
  432. headers.get(SENTRY_TRACE_HEADER_NAME)
  433. )
  434. if sentrytrace_kwargs is not None:
  435. kwargs.update(sentrytrace_kwargs)
  436. # If there's an incoming sentry-trace but no incoming baggage header,
  437. # for instance in traces coming from older SDKs,
  438. # baggage will be empty and immutable and won't be populated as head SDK.
  439. baggage.freeze()
  440. transaction = Transaction(**kwargs)
  441. transaction.same_process_as_parent = False
  442. return transaction
  443. def iter_headers(self):
  444. # type: () -> Iterator[Tuple[str, str]]
  445. """
  446. Creates a generator which returns the span's ``sentry-trace`` and ``baggage`` headers.
  447. If the span's containing transaction doesn't yet have a ``baggage`` value,
  448. this will cause one to be generated and stored.
  449. """
  450. if not self.containing_transaction:
  451. # Do not propagate headers if there is no containing transaction. Otherwise, this
  452. # span ends up being the root span of a new trace, and since it does not get sent
  453. # to Sentry, the trace will be missing a root transaction. The dynamic sampling
  454. # context will also be missing, breaking dynamic sampling & traces.
  455. return
  456. yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()
  457. baggage = self.containing_transaction.get_baggage().serialize()
  458. if baggage:
  459. yield BAGGAGE_HEADER_NAME, baggage
  460. @classmethod
  461. def from_traceparent(
  462. cls,
  463. traceparent, # type: Optional[str]
  464. **kwargs, # type: Any
  465. ):
  466. # type: (...) -> Optional[Transaction]
  467. """
  468. DEPRECATED: Use :py:meth:`sentry_sdk.tracing.Span.continue_from_headers`.
  469. Create a ``Transaction`` with the given params, then add in data pulled from
  470. the given ``sentry-trace`` header value before returning the ``Transaction``.
  471. """
  472. logger.warning(
  473. "Deprecated: Use Transaction.continue_from_headers(headers, **kwargs) "
  474. "instead of from_traceparent(traceparent, **kwargs)"
  475. )
  476. if not traceparent:
  477. return None
  478. return cls.continue_from_headers(
  479. {SENTRY_TRACE_HEADER_NAME: traceparent}, **kwargs
  480. )
  481. def to_traceparent(self):
  482. # type: () -> str
  483. if self.sampled is True:
  484. sampled = "1"
  485. elif self.sampled is False:
  486. sampled = "0"
  487. else:
  488. sampled = None
  489. traceparent = "%s-%s" % (self.trace_id, self.span_id)
  490. if sampled is not None:
  491. traceparent += "-%s" % (sampled,)
  492. return traceparent
  493. def to_baggage(self):
  494. # type: () -> Optional[Baggage]
  495. """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage`
  496. associated with this ``Span``, if any. (Taken from the root of the span tree.)
  497. """
  498. if self.containing_transaction:
  499. return self.containing_transaction.get_baggage()
  500. return None
  501. def set_tag(self, key, value):
  502. # type: (str, Any) -> None
  503. self._tags[key] = value
  504. def set_data(self, key, value):
  505. # type: (str, Any) -> None
  506. self._data[key] = value
  507. def set_flag(self, flag, result):
  508. # type: (str, bool) -> None
  509. if len(self._flags) < self._flags_capacity:
  510. self._flags[flag] = result
  511. def set_status(self, value):
  512. # type: (str) -> None
  513. self.status = value
  514. def set_measurement(self, name, value, unit=""):
  515. # type: (str, float, MeasurementUnit) -> None
  516. """
  517. .. deprecated:: 2.28.0
  518. This function is deprecated and will be removed in the next major release.
  519. """
  520. warnings.warn(
  521. "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.",
  522. DeprecationWarning,
  523. stacklevel=2,
  524. )
  525. self._measurements[name] = {"value": value, "unit": unit}
  526. def set_thread(self, thread_id, thread_name):
  527. # type: (Optional[int], Optional[str]) -> None
  528. if thread_id is not None:
  529. self.set_data(SPANDATA.THREAD_ID, str(thread_id))
  530. if thread_name is not None:
  531. self.set_data(SPANDATA.THREAD_NAME, thread_name)
  532. def set_profiler_id(self, profiler_id):
  533. # type: (Optional[str]) -> None
  534. if profiler_id is not None:
  535. self.set_data(SPANDATA.PROFILER_ID, profiler_id)
  536. def set_http_status(self, http_status):
  537. # type: (int) -> None
  538. self.set_tag(
  539. "http.status_code", str(http_status)
  540. ) # we keep this for backwards compatibility
  541. self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status)
  542. self.set_status(get_span_status_from_http_code(http_status))
  543. def is_success(self):
  544. # type: () -> bool
  545. return self.status == "ok"
  546. def finish(self, scope=None, end_timestamp=None):
  547. # type: (Optional[sentry_sdk.Scope], Optional[Union[float, datetime]]) -> Optional[str]
  548. """
  549. Sets the end timestamp of the span.
  550. Additionally it also creates a breadcrumb from the span,
  551. if the span represents a database or HTTP request.
  552. :param scope: The scope to use for this transaction.
  553. If not provided, the current scope will be used.
  554. :param end_timestamp: Optional timestamp that should
  555. be used as timestamp instead of the current time.
  556. :return: Always ``None``. The type is ``Optional[str]`` to match
  557. the return value of :py:meth:`sentry_sdk.tracing.Transaction.finish`.
  558. """
  559. if self.timestamp is not None:
  560. # This span is already finished, ignore.
  561. return None
  562. try:
  563. if end_timestamp:
  564. if isinstance(end_timestamp, float):
  565. end_timestamp = datetime.fromtimestamp(end_timestamp, timezone.utc)
  566. self.timestamp = end_timestamp
  567. else:
  568. elapsed = nanosecond_time() - self._start_timestamp_monotonic_ns
  569. self.timestamp = self.start_timestamp + timedelta(
  570. microseconds=elapsed / 1000
  571. )
  572. except AttributeError:
  573. self.timestamp = datetime.now(timezone.utc)
  574. scope = scope or sentry_sdk.get_current_scope()
  575. maybe_create_breadcrumbs_from_span(scope, self)
  576. return None
  577. def to_json(self):
  578. # type: () -> Dict[str, Any]
  579. """Returns a JSON-compatible representation of the span."""
  580. rv = {
  581. "trace_id": self.trace_id,
  582. "span_id": self.span_id,
  583. "parent_span_id": self.parent_span_id,
  584. "same_process_as_parent": self.same_process_as_parent,
  585. "op": self.op,
  586. "description": self.description,
  587. "start_timestamp": self.start_timestamp,
  588. "timestamp": self.timestamp,
  589. "origin": self.origin,
  590. } # type: Dict[str, Any]
  591. if self.status:
  592. self._tags["status"] = self.status
  593. if self._local_aggregator is not None:
  594. metrics_summary = self._local_aggregator.to_json()
  595. if metrics_summary:
  596. rv["_metrics_summary"] = metrics_summary
  597. if len(self._measurements) > 0:
  598. rv["measurements"] = self._measurements
  599. tags = self._tags
  600. if tags:
  601. rv["tags"] = tags
  602. data = {}
  603. data.update(self._flags)
  604. data.update(self._data)
  605. if data:
  606. rv["data"] = data
  607. return rv
  608. def get_trace_context(self):
  609. # type: () -> Any
  610. rv = {
  611. "trace_id": self.trace_id,
  612. "span_id": self.span_id,
  613. "parent_span_id": self.parent_span_id,
  614. "op": self.op,
  615. "description": self.description,
  616. "origin": self.origin,
  617. } # type: Dict[str, Any]
  618. if self.status:
  619. rv["status"] = self.status
  620. if self.containing_transaction:
  621. rv["dynamic_sampling_context"] = (
  622. self.containing_transaction.get_baggage().dynamic_sampling_context()
  623. )
  624. data = {}
  625. thread_id = self._data.get(SPANDATA.THREAD_ID)
  626. if thread_id is not None:
  627. data["thread.id"] = thread_id
  628. thread_name = self._data.get(SPANDATA.THREAD_NAME)
  629. if thread_name is not None:
  630. data["thread.name"] = thread_name
  631. if data:
  632. rv["data"] = data
  633. return rv
  634. def get_profile_context(self):
  635. # type: () -> Optional[ProfileContext]
  636. profiler_id = self._data.get(SPANDATA.PROFILER_ID)
  637. if profiler_id is None:
  638. return None
  639. return {
  640. "profiler_id": profiler_id,
  641. }
  642. def update_active_thread(self):
  643. # type: () -> None
  644. thread_id, thread_name = get_current_thread_meta()
  645. self.set_thread(thread_id, thread_name)
  646. class Transaction(Span):
  647. """The Transaction is the root element that holds all the spans
  648. for Sentry performance instrumentation.
  649. :param name: Identifier of the transaction.
  650. Will show up in the Sentry UI.
  651. :param parent_sampled: Whether the parent transaction was sampled.
  652. If True this transaction will be kept, if False it will be discarded.
  653. :param baggage: The W3C baggage header value.
  654. (see https://www.w3.org/TR/baggage/)
  655. :param source: A string describing the source of the transaction name.
  656. This will be used to determine the transaction's type.
  657. See https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
  658. for more information. Default "custom".
  659. :param kwargs: Additional arguments to be passed to the Span constructor.
  660. See :py:class:`sentry_sdk.tracing.Span` for available arguments.
  661. """
  662. __slots__ = (
  663. "name",
  664. "source",
  665. "parent_sampled",
  666. # used to create baggage value for head SDKs in dynamic sampling
  667. "sample_rate",
  668. "_measurements",
  669. "_contexts",
  670. "_profile",
  671. "_continuous_profile",
  672. "_baggage",
  673. "_sample_rand",
  674. )
  675. def __init__( # type: ignore[misc]
  676. self,
  677. name="", # type: str
  678. parent_sampled=None, # type: Optional[bool]
  679. baggage=None, # type: Optional[Baggage]
  680. source=TransactionSource.CUSTOM, # type: str
  681. **kwargs, # type: Unpack[SpanKwargs]
  682. ):
  683. # type: (...) -> None
  684. super().__init__(**kwargs)
  685. self.name = name
  686. self.source = source
  687. self.sample_rate = None # type: Optional[float]
  688. self.parent_sampled = parent_sampled
  689. self._measurements = {} # type: Dict[str, MeasurementValue]
  690. self._contexts = {} # type: Dict[str, Any]
  691. self._profile = None # type: Optional[Profile]
  692. self._continuous_profile = None # type: Optional[ContinuousProfile]
  693. self._baggage = baggage
  694. baggage_sample_rand = (
  695. None if self._baggage is None else self._baggage._sample_rand()
  696. )
  697. if baggage_sample_rand is not None:
  698. self._sample_rand = baggage_sample_rand
  699. else:
  700. self._sample_rand = _generate_sample_rand(self.trace_id)
  701. def __repr__(self):
  702. # type: () -> str
  703. return (
  704. "<%s(name=%r, op=%r, trace_id=%r, span_id=%r, parent_span_id=%r, sampled=%r, source=%r, origin=%r)>"
  705. % (
  706. self.__class__.__name__,
  707. self.name,
  708. self.op,
  709. self.trace_id,
  710. self.span_id,
  711. self.parent_span_id,
  712. self.sampled,
  713. self.source,
  714. self.origin,
  715. )
  716. )
  717. def _possibly_started(self):
  718. # type: () -> bool
  719. """Returns whether the transaction might have been started.
  720. If this returns False, we know that the transaction was not started
  721. with sentry_sdk.start_transaction, and therefore the transaction will
  722. be discarded.
  723. """
  724. # We must explicitly check self.sampled is False since self.sampled can be None
  725. return self._span_recorder is not None or self.sampled is False
  726. def __enter__(self):
  727. # type: () -> Transaction
  728. if not self._possibly_started():
  729. logger.debug(
  730. "Transaction was entered without being started with sentry_sdk.start_transaction."
  731. "The transaction will not be sent to Sentry. To fix, start the transaction by"
  732. "passing it to sentry_sdk.start_transaction."
  733. )
  734. super().__enter__()
  735. if self._profile is not None:
  736. self._profile.__enter__()
  737. return self
  738. def __exit__(self, ty, value, tb):
  739. # type: (Optional[Any], Optional[Any], Optional[Any]) -> None
  740. if self._profile is not None:
  741. self._profile.__exit__(ty, value, tb)
  742. if self._continuous_profile is not None:
  743. self._continuous_profile.stop()
  744. super().__exit__(ty, value, tb)
  745. @property
  746. def containing_transaction(self):
  747. # type: () -> Transaction
  748. """The root element of the span tree.
  749. In the case of a transaction it is the transaction itself.
  750. """
  751. # Transactions (as spans) belong to themselves (as transactions). This
  752. # is a getter rather than a regular attribute to avoid having a circular
  753. # reference.
  754. return self
  755. def _get_scope_from_finish_args(
  756. self,
  757. scope_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]
  758. hub_arg, # type: Optional[Union[sentry_sdk.Scope, sentry_sdk.Hub]]
  759. ):
  760. # type: (...) -> Optional[sentry_sdk.Scope]
  761. """
  762. Logic to get the scope from the arguments passed to finish. This
  763. function exists for backwards compatibility with the old finish.
  764. TODO: Remove this function in the next major version.
  765. """
  766. scope_or_hub = scope_arg
  767. if hub_arg is not None:
  768. warnings.warn(
  769. "The `hub` parameter is deprecated. Please use the `scope` parameter, instead.",
  770. DeprecationWarning,
  771. stacklevel=3,
  772. )
  773. scope_or_hub = hub_arg
  774. if isinstance(scope_or_hub, sentry_sdk.Hub):
  775. warnings.warn(
  776. "Passing a Hub to finish is deprecated. Please pass a Scope, instead.",
  777. DeprecationWarning,
  778. stacklevel=3,
  779. )
  780. return scope_or_hub.scope
  781. return scope_or_hub
  782. def finish(
  783. self,
  784. scope=None, # type: Optional[sentry_sdk.Scope]
  785. end_timestamp=None, # type: Optional[Union[float, datetime]]
  786. *,
  787. hub=None, # type: Optional[sentry_sdk.Hub]
  788. ):
  789. # type: (...) -> Optional[str]
  790. """Finishes the transaction and sends it to Sentry.
  791. All finished spans in the transaction will also be sent to Sentry.
  792. :param scope: The Scope to use for this transaction.
  793. If not provided, the current Scope will be used.
  794. :param end_timestamp: Optional timestamp that should
  795. be used as timestamp instead of the current time.
  796. :param hub: The hub to use for this transaction.
  797. This argument is DEPRECATED. Please use the `scope`
  798. parameter, instead.
  799. :return: The event ID if the transaction was sent to Sentry,
  800. otherwise None.
  801. """
  802. if self.timestamp is not None:
  803. # This transaction is already finished, ignore.
  804. return None
  805. # For backwards compatibility, we must handle the case where `scope`
  806. # or `hub` could both either be a `Scope` or a `Hub`.
  807. scope = self._get_scope_from_finish_args(
  808. scope, hub
  809. ) # type: Optional[sentry_sdk.Scope]
  810. scope = scope or self.scope or sentry_sdk.get_current_scope()
  811. client = sentry_sdk.get_client()
  812. if not client.is_active():
  813. # We have no active client and therefore nowhere to send this transaction.
  814. return None
  815. if self._span_recorder is None:
  816. # Explicit check against False needed because self.sampled might be None
  817. if self.sampled is False:
  818. logger.debug("Discarding transaction because sampled = False")
  819. else:
  820. logger.debug(
  821. "Discarding transaction because it was not started with sentry_sdk.start_transaction"
  822. )
  823. # This is not entirely accurate because discards here are not
  824. # exclusively based on sample rate but also traces sampler, but
  825. # we handle this the same here.
  826. if client.transport and has_tracing_enabled(client.options):
  827. if client.monitor and client.monitor.downsample_factor > 0:
  828. reason = "backpressure"
  829. else:
  830. reason = "sample_rate"
  831. client.transport.record_lost_event(reason, data_category="transaction")
  832. # Only one span (the transaction itself) is discarded, since we did not record any spans here.
  833. client.transport.record_lost_event(reason, data_category="span")
  834. return None
  835. if not self.name:
  836. logger.warning(
  837. "Transaction has no name, falling back to `<unlabeled transaction>`."
  838. )
  839. self.name = "<unlabeled transaction>"
  840. super().finish(scope, end_timestamp)
  841. if not self.sampled:
  842. # At this point a `sampled = None` should have already been resolved
  843. # to a concrete decision.
  844. if self.sampled is None:
  845. logger.warning("Discarding transaction without sampling decision.")
  846. return None
  847. finished_spans = [
  848. span.to_json()
  849. for span in self._span_recorder.spans
  850. if span.timestamp is not None
  851. ]
  852. len_diff = len(self._span_recorder.spans) - len(finished_spans)
  853. dropped_spans = len_diff + self._span_recorder.dropped_spans
  854. # we do this to break the circular reference of transaction -> span
  855. # recorder -> span -> containing transaction (which is where we started)
  856. # before either the spans or the transaction goes out of scope and has
  857. # to be garbage collected
  858. self._span_recorder = None
  859. contexts = {}
  860. contexts.update(self._contexts)
  861. contexts.update({"trace": self.get_trace_context()})
  862. profile_context = self.get_profile_context()
  863. if profile_context is not None:
  864. contexts.update({"profile": profile_context})
  865. event = {
  866. "type": "transaction",
  867. "transaction": self.name,
  868. "transaction_info": {"source": self.source},
  869. "contexts": contexts,
  870. "tags": self._tags,
  871. "timestamp": self.timestamp,
  872. "start_timestamp": self.start_timestamp,
  873. "spans": finished_spans,
  874. } # type: Event
  875. if dropped_spans > 0:
  876. event["_dropped_spans"] = dropped_spans
  877. if self._profile is not None and self._profile.valid():
  878. event["profile"] = self._profile
  879. self._profile = None
  880. event["measurements"] = self._measurements
  881. # This is here since `to_json` is not invoked. This really should
  882. # be gone when we switch to onlyspans.
  883. if self._local_aggregator is not None:
  884. metrics_summary = self._local_aggregator.to_json()
  885. if metrics_summary:
  886. event["_metrics_summary"] = metrics_summary
  887. return scope.capture_event(event)
  888. def set_measurement(self, name, value, unit=""):
  889. # type: (str, float, MeasurementUnit) -> None
  890. """
  891. .. deprecated:: 2.28.0
  892. This function is deprecated and will be removed in the next major release.
  893. """
  894. warnings.warn(
  895. "`set_measurement()` is deprecated and will be removed in the next major version. Please use `set_data()` instead.",
  896. DeprecationWarning,
  897. stacklevel=2,
  898. )
  899. self._measurements[name] = {"value": value, "unit": unit}
  900. def set_context(self, key, value):
  901. # type: (str, dict[str, Any]) -> None
  902. """Sets a context. Transactions can have multiple contexts
  903. and they should follow the format described in the "Contexts Interface"
  904. documentation.
  905. :param key: The name of the context.
  906. :param value: The information about the context.
  907. """
  908. self._contexts[key] = value
  909. def set_http_status(self, http_status):
  910. # type: (int) -> None
  911. """Sets the status of the Transaction according to the given HTTP status.
  912. :param http_status: The HTTP status code."""
  913. super().set_http_status(http_status)
  914. self.set_context("response", {"status_code": http_status})
  915. def to_json(self):
  916. # type: () -> Dict[str, Any]
  917. """Returns a JSON-compatible representation of the transaction."""
  918. rv = super().to_json()
  919. rv["name"] = self.name
  920. rv["source"] = self.source
  921. rv["sampled"] = self.sampled
  922. return rv
  923. def get_trace_context(self):
  924. # type: () -> Any
  925. trace_context = super().get_trace_context()
  926. if self._data:
  927. trace_context["data"] = self._data
  928. return trace_context
  929. def get_baggage(self):
  930. # type: () -> Baggage
  931. """Returns the :py:class:`~sentry_sdk.tracing_utils.Baggage`
  932. associated with the Transaction.
  933. The first time a new baggage with Sentry items is made,
  934. it will be frozen."""
  935. if not self._baggage or self._baggage.mutable:
  936. self._baggage = Baggage.populate_from_transaction(self)
  937. return self._baggage
  938. def _set_initial_sampling_decision(self, sampling_context):
  939. # type: (SamplingContext) -> None
  940. """
  941. Sets the transaction's sampling decision, according to the following
  942. precedence rules:
  943. 1. If a sampling decision is passed to `start_transaction`
  944. (`start_transaction(name: "my transaction", sampled: True)`), that
  945. decision will be used, regardless of anything else
  946. 2. If `traces_sampler` is defined, its decision will be used. It can
  947. choose to keep or ignore any parent sampling decision, or use the
  948. sampling context data to make its own decision or to choose a sample
  949. rate for the transaction.
  950. 3. If `traces_sampler` is not defined, but there's a parent sampling
  951. decision, the parent sampling decision will be used.
  952. 4. If `traces_sampler` is not defined and there's no parent sampling
  953. decision, `traces_sample_rate` will be used.
  954. """
  955. client = sentry_sdk.get_client()
  956. transaction_description = "{op}transaction <{name}>".format(
  957. op=("<" + self.op + "> " if self.op else ""), name=self.name
  958. )
  959. # nothing to do if tracing is disabled
  960. if not has_tracing_enabled(client.options):
  961. self.sampled = False
  962. return
  963. # if the user has forced a sampling decision by passing a `sampled`
  964. # value when starting the transaction, go with that
  965. if self.sampled is not None:
  966. self.sample_rate = float(self.sampled)
  967. return
  968. # we would have bailed already if neither `traces_sampler` nor
  969. # `traces_sample_rate` were defined, so one of these should work; prefer
  970. # the hook if so
  971. sample_rate = (
  972. client.options["traces_sampler"](sampling_context)
  973. if callable(client.options.get("traces_sampler"))
  974. else (
  975. # default inheritance behavior
  976. sampling_context["parent_sampled"]
  977. if sampling_context["parent_sampled"] is not None
  978. else client.options["traces_sample_rate"]
  979. )
  980. )
  981. # Since this is coming from the user (or from a function provided by the
  982. # user), who knows what we might get. (The only valid values are
  983. # booleans or numbers between 0 and 1.)
  984. if not is_valid_sample_rate(sample_rate, source="Tracing"):
  985. logger.warning(
  986. "[Tracing] Discarding {transaction_description} because of invalid sample rate.".format(
  987. transaction_description=transaction_description,
  988. )
  989. )
  990. self.sampled = False
  991. return
  992. self.sample_rate = float(sample_rate)
  993. if client.monitor:
  994. self.sample_rate /= 2**client.monitor.downsample_factor
  995. # if the function returned 0 (or false), or if `traces_sample_rate` is
  996. # 0, it's a sign the transaction should be dropped
  997. if not self.sample_rate:
  998. logger.debug(
  999. "[Tracing] Discarding {transaction_description} because {reason}".format(
  1000. transaction_description=transaction_description,
  1001. reason=(
  1002. "traces_sampler returned 0 or False"
  1003. if callable(client.options.get("traces_sampler"))
  1004. else "traces_sample_rate is set to 0"
  1005. ),
  1006. )
  1007. )
  1008. self.sampled = False
  1009. return
  1010. # Now we roll the dice.
  1011. self.sampled = self._sample_rand < Decimal.from_float(self.sample_rate)
  1012. if self.sampled:
  1013. logger.debug(
  1014. "[Tracing] Starting {transaction_description}".format(
  1015. transaction_description=transaction_description,
  1016. )
  1017. )
  1018. else:
  1019. logger.debug(
  1020. "[Tracing] Discarding {transaction_description} because it's not included in the random sample (sampling rate = {sample_rate})".format(
  1021. transaction_description=transaction_description,
  1022. sample_rate=self.sample_rate,
  1023. )
  1024. )
  1025. class NoOpSpan(Span):
  1026. def __repr__(self):
  1027. # type: () -> str
  1028. return "<%s>" % self.__class__.__name__
  1029. @property
  1030. def containing_transaction(self):
  1031. # type: () -> Optional[Transaction]
  1032. return None
  1033. def start_child(self, instrumenter=INSTRUMENTER.SENTRY, **kwargs):
  1034. # type: (str, **Any) -> NoOpSpan
  1035. return NoOpSpan()
  1036. def to_traceparent(self):
  1037. # type: () -> str
  1038. return ""
  1039. def to_baggage(self):
  1040. # type: () -> Optional[Baggage]
  1041. return None
  1042. def get_baggage(self):
  1043. # type: () -> Optional[Baggage]
  1044. return None
  1045. def iter_headers(self):
  1046. # type: () -> Iterator[Tuple[str, str]]
  1047. return iter(())
  1048. def set_tag(self, key, value):
  1049. # type: (str, Any) -> None
  1050. pass
  1051. def set_data(self, key, value):
  1052. # type: (str, Any) -> None
  1053. pass
  1054. def set_status(self, value):
  1055. # type: (str) -> None
  1056. pass
  1057. def set_http_status(self, http_status):
  1058. # type: (int) -> None
  1059. pass
  1060. def is_success(self):
  1061. # type: () -> bool
  1062. return True
  1063. def to_json(self):
  1064. # type: () -> Dict[str, Any]
  1065. return {}
  1066. def get_trace_context(self):
  1067. # type: () -> Any
  1068. return {}
  1069. def get_profile_context(self):
  1070. # type: () -> Any
  1071. return {}
  1072. def finish(
  1073. self,
  1074. scope=None, # type: Optional[sentry_sdk.Scope]
  1075. end_timestamp=None, # type: Optional[Union[float, datetime]]
  1076. *,
  1077. hub=None, # type: Optional[sentry_sdk.Hub]
  1078. ):
  1079. # type: (...) -> Optional[str]
  1080. """
  1081. The `hub` parameter is deprecated. Please use the `scope` parameter, instead.
  1082. """
  1083. pass
  1084. def set_measurement(self, name, value, unit=""):
  1085. # type: (str, float, MeasurementUnit) -> None
  1086. pass
  1087. def set_context(self, key, value):
  1088. # type: (str, dict[str, Any]) -> None
  1089. pass
  1090. def init_span_recorder(self, maxlen):
  1091. # type: (int) -> None
  1092. pass
  1093. def _set_initial_sampling_decision(self, sampling_context):
  1094. # type: (SamplingContext) -> None
  1095. pass
  1096. if TYPE_CHECKING:
  1097. @overload
  1098. def trace(func=None):
  1099. # type: (None) -> Callable[[Callable[P, R]], Callable[P, R]]
  1100. pass
  1101. @overload
  1102. def trace(func):
  1103. # type: (Callable[P, R]) -> Callable[P, R]
  1104. pass
  1105. def trace(func=None):
  1106. # type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
  1107. """
  1108. Decorator to start a child span under the existing current transaction.
  1109. If there is no current transaction, then nothing will be traced.
  1110. .. code-block::
  1111. :caption: Usage
  1112. import sentry_sdk
  1113. @sentry_sdk.trace
  1114. def my_function():
  1115. ...
  1116. @sentry_sdk.trace
  1117. async def my_async_function():
  1118. ...
  1119. """
  1120. from sentry_sdk.tracing_utils import start_child_span_decorator
  1121. # This patterns allows usage of both @sentry_traced and @sentry_traced(...)
  1122. # See https://stackoverflow.com/questions/52126071/decorator-with-arguments-avoid-parenthesis-when-no-arguments/52126278
  1123. if func:
  1124. return start_child_span_decorator(func)
  1125. else:
  1126. return start_child_span_decorator
  1127. # Circular imports
  1128. from sentry_sdk.tracing_utils import (
  1129. Baggage,
  1130. EnvironHeaders,
  1131. extract_sentrytrace_data,
  1132. _generate_sample_rand,
  1133. has_tracing_enabled,
  1134. maybe_create_breadcrumbs_from_span,
  1135. )
  1136. with warnings.catch_warnings():
  1137. # The code in this file which uses `LocalAggregator` is only called from the deprecated `metrics` module.
  1138. warnings.simplefilter("ignore", DeprecationWarning)
  1139. from sentry_sdk.metrics import LocalAggregator