tracing_utils.py 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315
  1. import contextlib
  2. import functools
  3. import inspect
  4. import os
  5. import re
  6. import sys
  7. from collections.abc import Mapping, MutableMapping
  8. from datetime import timedelta
  9. from random import Random
  10. from urllib.parse import quote, unquote
  11. import uuid
  12. import sentry_sdk
  13. from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS, SPANTEMPLATE
  14. from sentry_sdk.utils import (
  15. capture_internal_exceptions,
  16. filename_for_module,
  17. logger,
  18. match_regex_list,
  19. qualname_from_function,
  20. safe_repr,
  21. to_string,
  22. try_convert,
  23. is_sentry_url,
  24. _is_external_source,
  25. _is_in_project_root,
  26. _module_in_list,
  27. )
  28. from typing import TYPE_CHECKING
  29. if TYPE_CHECKING:
  30. from typing import Any
  31. from typing import Dict
  32. from typing import Generator
  33. from typing import Optional
  34. from typing import Union
  35. from typing import Iterator
  36. from typing import Tuple
  37. from types import FrameType
  38. SENTRY_TRACE_REGEX = re.compile(
  39. "^[ \t]*" # whitespace
  40. "([0-9a-f]{32})?" # trace_id
  41. "-?([0-9a-f]{16})?" # span_id
  42. "-?([01])?" # sampled
  43. "[ \t]*$" # whitespace
  44. )
  45. # This is a normal base64 regex, modified to reflect that fact that we strip the
  46. # trailing = or == off
  47. base64_stripped = (
  48. # any of the characters in the base64 "alphabet", in multiples of 4
  49. "([a-zA-Z0-9+/]{4})*"
  50. # either nothing or 2 or 3 base64-alphabet characters (see
  51. # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for
  52. # why there's never only 1 extra character)
  53. "([a-zA-Z0-9+/]{2,3})?"
  54. )
  55. class EnvironHeaders(Mapping): # type: ignore
  56. def __init__(
  57. self,
  58. environ: "Mapping[str, str]",
  59. prefix: str = "HTTP_",
  60. ) -> None:
  61. self.environ = environ
  62. self.prefix = prefix
  63. def __getitem__(self, key: str) -> "Optional[Any]":
  64. return self.environ[self.prefix + key.replace("-", "_").upper()]
  65. def __len__(self) -> int:
  66. return sum(1 for _ in iter(self))
  67. def __iter__(self) -> "Generator[str, None, None]":
  68. for k in self.environ:
  69. if not isinstance(k, str):
  70. continue
  71. k = k.replace("-", "_").upper()
  72. if not k.startswith(self.prefix):
  73. continue
  74. yield k[len(self.prefix) :]
  75. def has_tracing_enabled(options: "Optional[Dict[str, Any]]") -> bool:
  76. """
  77. Returns True if either traces_sample_rate or traces_sampler is
  78. defined and enable_tracing is set and not false.
  79. """
  80. if options is None:
  81. return False
  82. return bool(
  83. options.get("enable_tracing") is not False
  84. and (
  85. options.get("traces_sample_rate") is not None
  86. or options.get("traces_sampler") is not None
  87. )
  88. )
  89. @contextlib.contextmanager
  90. def record_sql_queries(
  91. cursor: "Any",
  92. query: "Any",
  93. params_list: "Any",
  94. paramstyle: "Optional[str]",
  95. executemany: bool,
  96. record_cursor_repr: bool = False,
  97. span_origin: str = "manual",
  98. ) -> "Generator[sentry_sdk.tracing.Span, None, None]":
  99. # TODO: Bring back capturing of params by default
  100. if sentry_sdk.get_client().options["_experiments"].get("record_sql_params", False):
  101. if not params_list or params_list == [None]:
  102. params_list = None
  103. if paramstyle == "pyformat":
  104. paramstyle = "format"
  105. else:
  106. params_list = None
  107. paramstyle = None
  108. query = _format_sql(cursor, query)
  109. data = {}
  110. if params_list is not None:
  111. data["db.params"] = params_list
  112. if paramstyle is not None:
  113. data["db.paramstyle"] = paramstyle
  114. if executemany:
  115. data["db.executemany"] = True
  116. if record_cursor_repr and cursor is not None:
  117. data["db.cursor"] = cursor
  118. with capture_internal_exceptions():
  119. sentry_sdk.add_breadcrumb(message=query, category="query", data=data)
  120. with sentry_sdk.start_span(
  121. op=OP.DB,
  122. name=query,
  123. origin=span_origin,
  124. ) as span:
  125. for k, v in data.items():
  126. span.set_data(k, v)
  127. yield span
  128. def maybe_create_breadcrumbs_from_span(
  129. scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span"
  130. ) -> None:
  131. if span.op == OP.DB_REDIS:
  132. scope.add_breadcrumb(
  133. message=span.description, type="redis", category="redis", data=span._tags
  134. )
  135. elif span.op == OP.HTTP_CLIENT:
  136. level = None
  137. status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE)
  138. if status_code:
  139. if 500 <= status_code <= 599:
  140. level = "error"
  141. elif 400 <= status_code <= 499:
  142. level = "warning"
  143. if level:
  144. scope.add_breadcrumb(
  145. type="http", category="httplib", data=span._data, level=level
  146. )
  147. else:
  148. scope.add_breadcrumb(type="http", category="httplib", data=span._data)
  149. elif span.op == "subprocess":
  150. scope.add_breadcrumb(
  151. type="subprocess",
  152. category="subprocess",
  153. message=span.description,
  154. data=span._data,
  155. )
  156. def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]":
  157. try:
  158. return frame.f_code.co_filename
  159. except Exception:
  160. return None
  161. def _should_be_included(
  162. is_sentry_sdk_frame: bool,
  163. namespace: "Optional[str]",
  164. in_app_include: "Optional[list[str]]",
  165. in_app_exclude: "Optional[list[str]]",
  166. abs_path: "Optional[str]",
  167. project_root: "Optional[str]",
  168. ) -> bool:
  169. # in_app_include takes precedence over in_app_exclude
  170. should_be_included = _module_in_list(namespace, in_app_include)
  171. should_be_excluded = _is_external_source(abs_path) or _module_in_list(
  172. namespace, in_app_exclude
  173. )
  174. return not is_sentry_sdk_frame and (
  175. should_be_included
  176. or (_is_in_project_root(abs_path, project_root) and not should_be_excluded)
  177. )
  178. def add_source(
  179. span: "sentry_sdk.tracing.Span",
  180. project_root: "Optional[str]",
  181. in_app_include: "Optional[list[str]]",
  182. in_app_exclude: "Optional[list[str]]",
  183. ) -> None:
  184. """
  185. Adds OTel compatible source code information to the span
  186. """
  187. # Find the correct frame
  188. frame: "Union[FrameType, None]" = sys._getframe()
  189. while frame is not None:
  190. abs_path = _get_frame_module_abs_path(frame)
  191. try:
  192. namespace: "Optional[str]" = frame.f_globals.get("__name__")
  193. except Exception:
  194. namespace = None
  195. is_sentry_sdk_frame = namespace is not None and namespace.startswith(
  196. "sentry_sdk."
  197. )
  198. should_be_included = _should_be_included(
  199. is_sentry_sdk_frame=is_sentry_sdk_frame,
  200. namespace=namespace,
  201. in_app_include=in_app_include,
  202. in_app_exclude=in_app_exclude,
  203. abs_path=abs_path,
  204. project_root=project_root,
  205. )
  206. if should_be_included:
  207. break
  208. frame = frame.f_back
  209. else:
  210. frame = None
  211. # Set the data
  212. if frame is not None:
  213. try:
  214. lineno = frame.f_lineno
  215. except Exception:
  216. lineno = None
  217. if lineno is not None:
  218. span.set_data(SPANDATA.CODE_LINENO, frame.f_lineno)
  219. try:
  220. namespace = frame.f_globals.get("__name__")
  221. except Exception:
  222. namespace = None
  223. if namespace is not None:
  224. span.set_data(SPANDATA.CODE_NAMESPACE, namespace)
  225. filepath = _get_frame_module_abs_path(frame)
  226. if filepath is not None:
  227. if namespace is not None:
  228. in_app_path = filename_for_module(namespace, filepath)
  229. elif project_root is not None and filepath.startswith(project_root):
  230. in_app_path = filepath.replace(project_root, "").lstrip(os.sep)
  231. else:
  232. in_app_path = filepath
  233. span.set_data(SPANDATA.CODE_FILEPATH, in_app_path)
  234. try:
  235. code_function = frame.f_code.co_name
  236. except Exception:
  237. code_function = None
  238. if code_function is not None:
  239. span.set_data(SPANDATA.CODE_FUNCTION, frame.f_code.co_name)
  240. def add_query_source(span: "sentry_sdk.tracing.Span") -> None:
  241. """
  242. Adds OTel compatible source code information to a database query span
  243. """
  244. client = sentry_sdk.get_client()
  245. if not client.is_active():
  246. return
  247. if span.timestamp is None or span.start_timestamp is None:
  248. return
  249. should_add_query_source = client.options.get("enable_db_query_source", True)
  250. if not should_add_query_source:
  251. return
  252. duration = span.timestamp - span.start_timestamp
  253. threshold = client.options.get("db_query_source_threshold_ms", 0)
  254. slow_query = duration / timedelta(milliseconds=1) > threshold
  255. if not slow_query:
  256. return
  257. add_source(
  258. span=span,
  259. project_root=client.options["project_root"],
  260. in_app_include=client.options.get("in_app_include"),
  261. in_app_exclude=client.options.get("in_app_exclude"),
  262. )
  263. def add_http_request_source(span: "sentry_sdk.tracing.Span") -> None:
  264. """
  265. Adds OTel compatible source code information to a span for an outgoing HTTP request
  266. """
  267. client = sentry_sdk.get_client()
  268. if not client.is_active():
  269. return
  270. if span.timestamp is None or span.start_timestamp is None:
  271. return
  272. should_add_request_source = client.options.get("enable_http_request_source", True)
  273. if not should_add_request_source:
  274. return
  275. duration = span.timestamp - span.start_timestamp
  276. threshold = client.options.get("http_request_source_threshold_ms", 0)
  277. slow_query = duration / timedelta(milliseconds=1) > threshold
  278. if not slow_query:
  279. return
  280. add_source(
  281. span=span,
  282. project_root=client.options["project_root"],
  283. in_app_include=client.options.get("in_app_include"),
  284. in_app_exclude=client.options.get("in_app_exclude"),
  285. )
  286. def extract_sentrytrace_data(
  287. header: "Optional[str]",
  288. ) -> "Optional[Dict[str, Union[str, bool, None]]]":
  289. """
  290. Given a `sentry-trace` header string, return a dictionary of data.
  291. """
  292. if not header:
  293. return None
  294. if header.startswith("00-") and header.endswith("-00"):
  295. header = header[3:-3]
  296. match = SENTRY_TRACE_REGEX.match(header)
  297. if not match:
  298. return None
  299. trace_id, parent_span_id, sampled_str = match.groups()
  300. parent_sampled = None
  301. if trace_id:
  302. trace_id = "{:032x}".format(int(trace_id, 16))
  303. if parent_span_id:
  304. parent_span_id = "{:016x}".format(int(parent_span_id, 16))
  305. if sampled_str:
  306. parent_sampled = sampled_str != "0"
  307. return {
  308. "trace_id": trace_id,
  309. "parent_span_id": parent_span_id,
  310. "parent_sampled": parent_sampled,
  311. }
  312. def _format_sql(cursor: "Any", sql: str) -> "Optional[str]":
  313. real_sql = None
  314. # If we're using psycopg2, it could be that we're
  315. # looking at a query that uses Composed objects. Use psycopg2's mogrify
  316. # function to format the query. We lose per-parameter trimming but gain
  317. # accuracy in formatting.
  318. try:
  319. if hasattr(cursor, "mogrify"):
  320. real_sql = cursor.mogrify(sql)
  321. if isinstance(real_sql, bytes):
  322. real_sql = real_sql.decode(cursor.connection.encoding)
  323. except Exception:
  324. real_sql = None
  325. return real_sql or to_string(sql)
  326. class PropagationContext:
  327. """
  328. The PropagationContext represents the data of a trace in Sentry.
  329. """
  330. __slots__ = (
  331. "_trace_id",
  332. "_span_id",
  333. "parent_span_id",
  334. "parent_sampled",
  335. "baggage",
  336. )
  337. def __init__(
  338. self,
  339. trace_id: "Optional[str]" = None,
  340. span_id: "Optional[str]" = None,
  341. parent_span_id: "Optional[str]" = None,
  342. parent_sampled: "Optional[bool]" = None,
  343. dynamic_sampling_context: "Optional[Dict[str, str]]" = None,
  344. baggage: "Optional[Baggage]" = None,
  345. ) -> None:
  346. self._trace_id = trace_id
  347. """The trace id of the Sentry trace."""
  348. self._span_id = span_id
  349. """The span id of the currently executing span."""
  350. self.parent_span_id = parent_span_id
  351. """The id of the parent span that started this span.
  352. The parent span could also be a span in an upstream service."""
  353. self.parent_sampled = parent_sampled
  354. """Boolean indicator if the parent span was sampled.
  355. Important when the parent span originated in an upstream service,
  356. because we want to sample the whole trace, or nothing from the trace."""
  357. self.baggage = baggage
  358. """Parsed baggage header that is used for dynamic sampling decisions."""
  359. """DEPRECATED this only exists for backwards compat of constructor."""
  360. if baggage is None and dynamic_sampling_context is not None:
  361. self.baggage = Baggage(dynamic_sampling_context)
  362. @classmethod
  363. def from_incoming_data(
  364. cls, incoming_data: "Dict[str, Any]"
  365. ) -> "PropagationContext":
  366. propagation_context = PropagationContext()
  367. normalized_data = normalize_incoming_data(incoming_data)
  368. sentry_trace_header = normalized_data.get(SENTRY_TRACE_HEADER_NAME)
  369. sentrytrace_data = extract_sentrytrace_data(sentry_trace_header)
  370. # nothing to propagate if no sentry-trace
  371. if sentrytrace_data is None:
  372. return propagation_context
  373. baggage_header = normalized_data.get(BAGGAGE_HEADER_NAME)
  374. baggage = (
  375. Baggage.from_incoming_header(baggage_header) if baggage_header else None
  376. )
  377. if not _should_continue_trace(baggage):
  378. return propagation_context
  379. propagation_context.update(sentrytrace_data)
  380. if baggage:
  381. propagation_context.baggage = baggage
  382. propagation_context._fill_sample_rand()
  383. return propagation_context
  384. @property
  385. def trace_id(self) -> str:
  386. """The trace id of the Sentry trace."""
  387. if not self._trace_id:
  388. # New trace, don't fill in sample_rand
  389. self._trace_id = uuid.uuid4().hex
  390. return self._trace_id
  391. @trace_id.setter
  392. def trace_id(self, value: str) -> None:
  393. self._trace_id = value
  394. @property
  395. def span_id(self) -> str:
  396. """The span id of the currently executed span."""
  397. if not self._span_id:
  398. self._span_id = uuid.uuid4().hex[16:]
  399. return self._span_id
  400. @span_id.setter
  401. def span_id(self, value: str) -> None:
  402. self._span_id = value
  403. @property
  404. def dynamic_sampling_context(self) -> "Optional[Dict[str, Any]]":
  405. return self.get_baggage().dynamic_sampling_context()
  406. def to_traceparent(self) -> str:
  407. return f"{self.trace_id}-{self.span_id}"
  408. def get_baggage(self) -> "Baggage":
  409. if self.baggage is None:
  410. self.baggage = Baggage.populate_from_propagation_context(self)
  411. return self.baggage
  412. def iter_headers(self) -> "Iterator[Tuple[str, str]]":
  413. """
  414. Creates a generator which returns the propagation_context's ``sentry-trace`` and ``baggage`` headers.
  415. """
  416. yield SENTRY_TRACE_HEADER_NAME, self.to_traceparent()
  417. baggage = self.get_baggage().serialize()
  418. if baggage:
  419. yield BAGGAGE_HEADER_NAME, baggage
  420. def update(self, other_dict: "Dict[str, Any]") -> None:
  421. """
  422. Updates the PropagationContext with data from the given dictionary.
  423. """
  424. for key, value in other_dict.items():
  425. try:
  426. setattr(self, key, value)
  427. except AttributeError:
  428. pass
  429. def __repr__(self) -> str:
  430. return "<PropagationContext _trace_id={} _span_id={} parent_span_id={} parent_sampled={} baggage={}>".format(
  431. self._trace_id,
  432. self._span_id,
  433. self.parent_span_id,
  434. self.parent_sampled,
  435. self.baggage,
  436. )
  437. def _fill_sample_rand(self) -> None:
  438. """
  439. Ensure that there is a valid sample_rand value in the baggage.
  440. If there is a valid sample_rand value in the baggage, we keep it.
  441. Otherwise, we generate a sample_rand value according to the following:
  442. - If we have a parent_sampled value and a sample_rate in the DSC, we compute
  443. a sample_rand value randomly in the range:
  444. - [0, sample_rate) if parent_sampled is True,
  445. - or, in the range [sample_rate, 1) if parent_sampled is False.
  446. - If either parent_sampled or sample_rate is missing, we generate a random
  447. value in the range [0, 1).
  448. The sample_rand is deterministically generated from the trace_id, if present.
  449. This function does nothing if there is no baggage.
  450. """
  451. if self.baggage is None:
  452. return
  453. sample_rand = try_convert(float, self.baggage.sentry_items.get("sample_rand"))
  454. if sample_rand is not None and 0 <= sample_rand < 1:
  455. # sample_rand is present and valid, so don't overwrite it
  456. return
  457. # Get the sample rate and compute the transformation that will map the random value
  458. # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1).
  459. sample_rate = try_convert(float, self.baggage.sentry_items.get("sample_rate"))
  460. lower, upper = _sample_rand_range(self.parent_sampled, sample_rate)
  461. try:
  462. sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper))
  463. except ValueError:
  464. # ValueError is raised if the interval is invalid, i.e. lower >= upper.
  465. # lower >= upper might happen if the incoming trace's sampled flag
  466. # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True.
  467. # We cannot generate a sensible sample_rand value in this case.
  468. logger.debug(
  469. f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} "
  470. f"and sample_rate={sample_rate}."
  471. )
  472. return
  473. self.baggage.sentry_items["sample_rand"] = f"{sample_rand:.6f}" # noqa: E231
  474. def _sample_rand(self) -> "Optional[str]":
  475. """Convenience method to get the sample_rand value from the baggage."""
  476. if self.baggage is None:
  477. return None
  478. return self.baggage.sentry_items.get("sample_rand")
  479. class Baggage:
  480. """
  481. The W3C Baggage header information (see https://www.w3.org/TR/baggage/).
  482. Before mutating a `Baggage` object, calling code must check that `mutable` is `True`.
  483. Mutating a `Baggage` object that has `mutable` set to `False` is not allowed, but
  484. it is the caller's responsibility to enforce this restriction.
  485. """
  486. __slots__ = ("sentry_items", "third_party_items", "mutable")
  487. SENTRY_PREFIX = "sentry-"
  488. SENTRY_PREFIX_REGEX = re.compile("^sentry-")
  489. def __init__(
  490. self,
  491. sentry_items: "Dict[str, str]",
  492. third_party_items: str = "",
  493. mutable: bool = True,
  494. ):
  495. self.sentry_items = sentry_items
  496. self.third_party_items = third_party_items
  497. self.mutable = mutable
  498. @classmethod
  499. def from_incoming_header(
  500. cls,
  501. header: "Optional[str]",
  502. *,
  503. _sample_rand: "Optional[str]" = None,
  504. ) -> "Baggage":
  505. """
  506. freeze if incoming header already has sentry baggage
  507. """
  508. sentry_items = {}
  509. third_party_items = ""
  510. mutable = True
  511. if header:
  512. for item in header.split(","):
  513. if "=" not in item:
  514. continue
  515. with capture_internal_exceptions():
  516. item = item.strip()
  517. key, val = item.split("=")
  518. if Baggage.SENTRY_PREFIX_REGEX.match(key):
  519. baggage_key = unquote(key.split("-")[1])
  520. sentry_items[baggage_key] = unquote(val)
  521. mutable = False
  522. else:
  523. third_party_items += ("," if third_party_items else "") + item
  524. if _sample_rand is not None:
  525. sentry_items["sample_rand"] = str(_sample_rand)
  526. mutable = False
  527. return Baggage(sentry_items, third_party_items, mutable)
  528. @classmethod
  529. def from_options(cls, scope: "sentry_sdk.scope.Scope") -> "Optional[Baggage]":
  530. """
  531. Deprecated: use populate_from_propagation_context
  532. """
  533. if scope._propagation_context is None:
  534. return Baggage({})
  535. return Baggage.populate_from_propagation_context(scope._propagation_context)
  536. @classmethod
  537. def populate_from_propagation_context(
  538. cls, propagation_context: "PropagationContext"
  539. ) -> "Baggage":
  540. sentry_items: "Dict[str, str]" = {}
  541. third_party_items = ""
  542. mutable = False
  543. client = sentry_sdk.get_client()
  544. if not client.is_active():
  545. return Baggage(sentry_items)
  546. options = client.options
  547. sentry_items["trace_id"] = propagation_context.trace_id
  548. if options.get("environment"):
  549. sentry_items["environment"] = options["environment"]
  550. if options.get("release"):
  551. sentry_items["release"] = options["release"]
  552. if client.parsed_dsn:
  553. sentry_items["public_key"] = client.parsed_dsn.public_key
  554. if client.parsed_dsn.org_id:
  555. sentry_items["org_id"] = client.parsed_dsn.org_id
  556. if options.get("traces_sample_rate"):
  557. sentry_items["sample_rate"] = str(options["traces_sample_rate"])
  558. return Baggage(sentry_items, third_party_items, mutable)
  559. @classmethod
  560. def populate_from_transaction(
  561. cls, transaction: "sentry_sdk.tracing.Transaction"
  562. ) -> "Baggage":
  563. """
  564. Populate fresh baggage entry with sentry_items and make it immutable
  565. if this is the head SDK which originates traces.
  566. """
  567. client = sentry_sdk.get_client()
  568. sentry_items: "Dict[str, str]" = {}
  569. if not client.is_active():
  570. return Baggage(sentry_items)
  571. options = client.options or {}
  572. sentry_items["trace_id"] = transaction.trace_id
  573. sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231
  574. if options.get("environment"):
  575. sentry_items["environment"] = options["environment"]
  576. if options.get("release"):
  577. sentry_items["release"] = options["release"]
  578. if client.parsed_dsn:
  579. sentry_items["public_key"] = client.parsed_dsn.public_key
  580. if client.parsed_dsn.org_id:
  581. sentry_items["org_id"] = client.parsed_dsn.org_id
  582. if (
  583. transaction.name
  584. and transaction.source not in LOW_QUALITY_TRANSACTION_SOURCES
  585. ):
  586. sentry_items["transaction"] = transaction.name
  587. if transaction.sample_rate is not None:
  588. sentry_items["sample_rate"] = str(transaction.sample_rate)
  589. if transaction.sampled is not None:
  590. sentry_items["sampled"] = "true" if transaction.sampled else "false"
  591. # there's an existing baggage but it was mutable,
  592. # which is why we are creating this new baggage.
  593. # However, if by chance the user put some sentry items in there, give them precedence.
  594. if transaction._baggage and transaction._baggage.sentry_items:
  595. sentry_items.update(transaction._baggage.sentry_items)
  596. return Baggage(sentry_items, mutable=False)
  597. def freeze(self) -> None:
  598. self.mutable = False
  599. def dynamic_sampling_context(self) -> "Dict[str, str]":
  600. header = {}
  601. for key, item in self.sentry_items.items():
  602. header[key] = item
  603. return header
  604. def serialize(self, include_third_party: bool = False) -> str:
  605. items = []
  606. for key, val in self.sentry_items.items():
  607. with capture_internal_exceptions():
  608. item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val))
  609. items.append(item)
  610. if include_third_party:
  611. items.append(self.third_party_items)
  612. return ",".join(items)
  613. @staticmethod
  614. def strip_sentry_baggage(header: str) -> str:
  615. """Remove Sentry baggage from the given header.
  616. Given a Baggage header, return a new Baggage header with all Sentry baggage items removed.
  617. """
  618. return ",".join(
  619. (
  620. item
  621. for item in header.split(",")
  622. if not Baggage.SENTRY_PREFIX_REGEX.match(item.strip())
  623. )
  624. )
  625. def _sample_rand(self) -> "Optional[float]":
  626. """Convenience method to get the sample_rand value from the sentry_items.
  627. We validate the value and parse it as a float before returning it. The value is considered
  628. valid if it is a float in the range [0, 1).
  629. """
  630. sample_rand = try_convert(float, self.sentry_items.get("sample_rand"))
  631. if sample_rand is not None and 0.0 <= sample_rand < 1.0:
  632. return sample_rand
  633. return None
  634. def __repr__(self) -> str:
  635. return f'<Baggage "{self.serialize(include_third_party=True)}", mutable={self.mutable}>'
  636. def should_propagate_trace(client: "sentry_sdk.client.BaseClient", url: str) -> bool:
  637. """
  638. Returns True if url matches trace_propagation_targets configured in the given client. Otherwise, returns False.
  639. """
  640. trace_propagation_targets = client.options["trace_propagation_targets"]
  641. if is_sentry_url(client, url):
  642. return False
  643. return match_regex_list(url, trace_propagation_targets, substring_matching=True)
  644. def normalize_incoming_data(incoming_data: "Dict[str, Any]") -> "Dict[str, Any]":
  645. """
  646. Normalizes incoming data so the keys are all lowercase with dashes instead of underscores and stripped from known prefixes.
  647. """
  648. data = {}
  649. for key, value in incoming_data.items():
  650. if key.startswith("HTTP_"):
  651. key = key[5:]
  652. key = key.replace("_", "-").lower()
  653. data[key] = value
  654. return data
  655. def create_span_decorator(
  656. op: "Optional[Union[str, OP]]" = None,
  657. name: "Optional[str]" = None,
  658. attributes: "Optional[dict[str, Any]]" = None,
  659. template: "SPANTEMPLATE" = SPANTEMPLATE.DEFAULT,
  660. ) -> "Any":
  661. """
  662. Create a span decorator that can wrap both sync and async functions.
  663. :param op: The operation type for the span.
  664. :type op: str or :py:class:`sentry_sdk.consts.OP` or None
  665. :param name: The name of the span.
  666. :type name: str or None
  667. :param attributes: Additional attributes to set on the span.
  668. :type attributes: dict or None
  669. :param template: The type of span to create. This determines what kind of
  670. span instrumentation and data collection will be applied. Use predefined
  671. constants from :py:class:`sentry_sdk.consts.SPANTEMPLATE`.
  672. The default is `SPANTEMPLATE.DEFAULT` which is the right choice for most
  673. use cases.
  674. :type template: :py:class:`sentry_sdk.consts.SPANTEMPLATE`
  675. """
  676. from sentry_sdk.scope import should_send_default_pii
  677. def span_decorator(f: "Any") -> "Any":
  678. """
  679. Decorator to create a span for the given function.
  680. """
  681. @functools.wraps(f)
  682. async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
  683. current_span = get_current_span()
  684. if current_span is None:
  685. logger.debug(
  686. "Cannot create a child span for %s. "
  687. "Please start a Sentry transaction before calling this function.",
  688. qualname_from_function(f),
  689. )
  690. return await f(*args, **kwargs)
  691. span_op = op or _get_span_op(template)
  692. function_name = name or qualname_from_function(f) or ""
  693. span_name = _get_span_name(template, function_name, kwargs)
  694. send_pii = should_send_default_pii()
  695. with current_span.start_child(
  696. op=span_op,
  697. name=span_name,
  698. ) as span:
  699. span.update_data(attributes or {})
  700. _set_input_attributes(
  701. span, template, send_pii, function_name, f, args, kwargs
  702. )
  703. result = await f(*args, **kwargs)
  704. _set_output_attributes(span, template, send_pii, result)
  705. return result
  706. try:
  707. async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
  708. except Exception:
  709. pass
  710. @functools.wraps(f)
  711. def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
  712. current_span = get_current_span()
  713. if current_span is None:
  714. logger.debug(
  715. "Cannot create a child span for %s. "
  716. "Please start a Sentry transaction before calling this function.",
  717. qualname_from_function(f),
  718. )
  719. return f(*args, **kwargs)
  720. span_op = op or _get_span_op(template)
  721. function_name = name or qualname_from_function(f) or ""
  722. span_name = _get_span_name(template, function_name, kwargs)
  723. send_pii = should_send_default_pii()
  724. with current_span.start_child(
  725. op=span_op,
  726. name=span_name,
  727. ) as span:
  728. span.update_data(attributes or {})
  729. _set_input_attributes(
  730. span, template, send_pii, function_name, f, args, kwargs
  731. )
  732. result = f(*args, **kwargs)
  733. _set_output_attributes(span, template, send_pii, result)
  734. return result
  735. try:
  736. sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
  737. except Exception:
  738. pass
  739. if inspect.iscoroutinefunction(f):
  740. return async_wrapper
  741. else:
  742. return sync_wrapper
  743. return span_decorator
  744. def get_current_span(scope: "Optional[sentry_sdk.Scope]" = None) -> "Optional[Span]":
  745. """
  746. Returns the currently active span if there is one running, otherwise `None`
  747. """
  748. scope = scope or sentry_sdk.get_current_scope()
  749. current_span = scope.span
  750. return current_span
  751. def set_span_errored(span: "Optional[Span]" = None) -> None:
  752. """
  753. Set the status of the current or given span to INTERNAL_ERROR.
  754. Also sets the status of the transaction (root span) to INTERNAL_ERROR.
  755. """
  756. span = span or get_current_span()
  757. if span is not None:
  758. span.set_status(SPANSTATUS.INTERNAL_ERROR)
  759. if span.containing_transaction is not None:
  760. span.containing_transaction.set_status(SPANSTATUS.INTERNAL_ERROR)
  761. def _generate_sample_rand(
  762. trace_id: "Optional[str]",
  763. *,
  764. interval: "tuple[float, float]" = (0.0, 1.0),
  765. ) -> float:
  766. """Generate a sample_rand value from a trace ID.
  767. The generated value will be pseudorandomly chosen from the provided
  768. interval. Specifically, given (lower, upper) = interval, the generated
  769. value will be in the range [lower, upper). The value has 6-digit precision,
  770. so when printing with .6f, the value will never be rounded up.
  771. The pseudorandom number generator is seeded with the trace ID.
  772. """
  773. lower, upper = interval
  774. if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly
  775. raise ValueError("Invalid interval: lower must be less than upper")
  776. rng = Random(trace_id)
  777. lower_scaled = int(lower * 1_000_000)
  778. upper_scaled = int(upper * 1_000_000)
  779. try:
  780. sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled)
  781. except ValueError:
  782. # In some corner cases it might happen that the range is too small
  783. # In that case, just take the lower bound
  784. sample_rand_scaled = lower_scaled
  785. return sample_rand_scaled / 1_000_000
  786. def _sample_rand_range(
  787. parent_sampled: "Optional[bool]", sample_rate: "Optional[float]"
  788. ) -> "tuple[float, float]":
  789. """
  790. Compute the lower (inclusive) and upper (exclusive) bounds of the range of values
  791. that a generated sample_rand value must fall into, given the parent_sampled and
  792. sample_rate values.
  793. """
  794. if parent_sampled is None or sample_rate is None:
  795. return 0.0, 1.0
  796. elif parent_sampled is True:
  797. return 0.0, sample_rate
  798. else: # parent_sampled is False
  799. return sample_rate, 1.0
  800. def _get_value(source: "Any", key: str) -> "Optional[Any]":
  801. """
  802. Gets a value from a source object. The source can be a dict or an object.
  803. It is checked for dictionary keys and object attributes.
  804. """
  805. value = None
  806. if isinstance(source, dict):
  807. value = source.get(key)
  808. else:
  809. if hasattr(source, key):
  810. try:
  811. value = getattr(source, key)
  812. except Exception:
  813. value = None
  814. return value
  815. def _get_span_name(
  816. template: "Union[str, SPANTEMPLATE]",
  817. name: str,
  818. kwargs: "Optional[dict[str, Any]]" = None,
  819. ) -> str:
  820. """
  821. Get the name of the span based on the template and the name.
  822. """
  823. span_name = name
  824. if template == SPANTEMPLATE.AI_CHAT:
  825. model = None
  826. if kwargs:
  827. for key in ("model", "model_name"):
  828. if kwargs.get(key) and isinstance(kwargs[key], str):
  829. model = kwargs[key]
  830. break
  831. span_name = f"chat {model}" if model else "chat"
  832. elif template == SPANTEMPLATE.AI_AGENT:
  833. span_name = f"invoke_agent {name}"
  834. elif template == SPANTEMPLATE.AI_TOOL:
  835. span_name = f"execute_tool {name}"
  836. return span_name
  837. def _get_span_op(template: "Union[str, SPANTEMPLATE]") -> str:
  838. """
  839. Get the operation of the span based on the template.
  840. """
  841. mapping: "dict[Union[str, SPANTEMPLATE], Union[str, OP]]" = {
  842. SPANTEMPLATE.AI_CHAT: OP.GEN_AI_CHAT,
  843. SPANTEMPLATE.AI_AGENT: OP.GEN_AI_INVOKE_AGENT,
  844. SPANTEMPLATE.AI_TOOL: OP.GEN_AI_EXECUTE_TOOL,
  845. }
  846. op = mapping.get(template, OP.FUNCTION)
  847. return str(op)
  848. def _get_input_attributes(
  849. template: "Union[str, SPANTEMPLATE]",
  850. send_pii: bool,
  851. args: "tuple[Any, ...]",
  852. kwargs: "dict[str, Any]",
  853. ) -> "dict[str, Any]":
  854. """
  855. Get input attributes for the given span template.
  856. """
  857. attributes: "dict[str, Any]" = {}
  858. if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]:
  859. mapping = {
  860. "model": (SPANDATA.GEN_AI_REQUEST_MODEL, str),
  861. "model_name": (SPANDATA.GEN_AI_REQUEST_MODEL, str),
  862. "agent": (SPANDATA.GEN_AI_AGENT_NAME, str),
  863. "agent_name": (SPANDATA.GEN_AI_AGENT_NAME, str),
  864. "max_tokens": (SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, int),
  865. "frequency_penalty": (SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY, float),
  866. "presence_penalty": (SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY, float),
  867. "temperature": (SPANDATA.GEN_AI_REQUEST_TEMPERATURE, float),
  868. "top_p": (SPANDATA.GEN_AI_REQUEST_TOP_P, float),
  869. "top_k": (SPANDATA.GEN_AI_REQUEST_TOP_K, int),
  870. }
  871. def _set_from_key(key: str, value: "Any") -> None:
  872. if key in mapping:
  873. (attribute, data_type) = mapping[key]
  874. if value is not None and isinstance(value, data_type):
  875. attributes[attribute] = value
  876. for key, value in list(kwargs.items()):
  877. if key == "prompt" and isinstance(value, str):
  878. attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append(
  879. {"role": "user", "content": value}
  880. )
  881. continue
  882. if key == "system_prompt" and isinstance(value, str):
  883. attributes.setdefault(SPANDATA.GEN_AI_REQUEST_MESSAGES, []).append(
  884. {"role": "system", "content": value}
  885. )
  886. continue
  887. _set_from_key(key, value)
  888. if template == SPANTEMPLATE.AI_TOOL and send_pii:
  889. attributes[SPANDATA.GEN_AI_TOOL_INPUT] = safe_repr(
  890. {"args": args, "kwargs": kwargs}
  891. )
  892. # Coerce to string
  893. if SPANDATA.GEN_AI_REQUEST_MESSAGES in attributes:
  894. attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES] = safe_repr(
  895. attributes[SPANDATA.GEN_AI_REQUEST_MESSAGES]
  896. )
  897. return attributes
  898. def _get_usage_attributes(usage: "Any") -> "dict[str, Any]":
  899. """
  900. Get usage attributes.
  901. """
  902. attributes = {}
  903. def _set_from_keys(attribute: str, keys: "tuple[str, ...]") -> None:
  904. for key in keys:
  905. value = _get_value(usage, key)
  906. if value is not None and isinstance(value, int):
  907. attributes[attribute] = value
  908. _set_from_keys(
  909. SPANDATA.GEN_AI_USAGE_INPUT_TOKENS,
  910. ("prompt_tokens", "input_tokens"),
  911. )
  912. _set_from_keys(
  913. SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS,
  914. ("completion_tokens", "output_tokens"),
  915. )
  916. _set_from_keys(
  917. SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS,
  918. ("total_tokens",),
  919. )
  920. return attributes
  921. def _get_output_attributes(
  922. template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any"
  923. ) -> "dict[str, Any]":
  924. """
  925. Get output attributes for the given span template.
  926. """
  927. attributes: "dict[str, Any]" = {}
  928. if template in [SPANTEMPLATE.AI_AGENT, SPANTEMPLATE.AI_TOOL, SPANTEMPLATE.AI_CHAT]:
  929. with capture_internal_exceptions():
  930. # Usage from result, result.usage, and result.metadata.usage
  931. usage_candidates = [result]
  932. usage = _get_value(result, "usage")
  933. usage_candidates.append(usage)
  934. meta = _get_value(result, "metadata")
  935. usage = _get_value(meta, "usage")
  936. usage_candidates.append(usage)
  937. for usage_candidate in usage_candidates:
  938. if usage_candidate is not None:
  939. attributes.update(_get_usage_attributes(usage_candidate))
  940. # Response model
  941. model_name = _get_value(result, "model")
  942. if model_name is not None and isinstance(model_name, str):
  943. attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name
  944. model_name = _get_value(result, "model_name")
  945. if model_name is not None and isinstance(model_name, str):
  946. attributes[SPANDATA.GEN_AI_RESPONSE_MODEL] = model_name
  947. # Tool output
  948. if template == SPANTEMPLATE.AI_TOOL and send_pii:
  949. attributes[SPANDATA.GEN_AI_TOOL_OUTPUT] = safe_repr(result)
  950. return attributes
  951. def _set_input_attributes(
  952. span: "Span",
  953. template: "Union[str, SPANTEMPLATE]",
  954. send_pii: bool,
  955. name: str,
  956. f: "Any",
  957. args: "tuple[Any, ...]",
  958. kwargs: "dict[str, Any]",
  959. ) -> None:
  960. """
  961. Set span input attributes based on the given span template.
  962. :param span: The span to set attributes on.
  963. :param template: The template to use to set attributes on the span.
  964. :param send_pii: Whether to send PII data.
  965. :param f: The wrapped function.
  966. :param args: The arguments to the wrapped function.
  967. :param kwargs: The keyword arguments to the wrapped function.
  968. """
  969. attributes: "dict[str, Any]" = {}
  970. if template == SPANTEMPLATE.AI_AGENT:
  971. attributes = {
  972. SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent",
  973. SPANDATA.GEN_AI_AGENT_NAME: name,
  974. }
  975. elif template == SPANTEMPLATE.AI_CHAT:
  976. attributes = {
  977. SPANDATA.GEN_AI_OPERATION_NAME: "chat",
  978. }
  979. elif template == SPANTEMPLATE.AI_TOOL:
  980. attributes = {
  981. SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
  982. SPANDATA.GEN_AI_TOOL_NAME: name,
  983. }
  984. docstring = f.__doc__
  985. if docstring is not None:
  986. attributes[SPANDATA.GEN_AI_TOOL_DESCRIPTION] = docstring
  987. attributes.update(_get_input_attributes(template, send_pii, args, kwargs))
  988. span.update_data(attributes or {})
  989. def _set_output_attributes(
  990. span: "Span", template: "Union[str, SPANTEMPLATE]", send_pii: bool, result: "Any"
  991. ) -> None:
  992. """
  993. Set span output attributes based on the given span template.
  994. :param span: The span to set attributes on.
  995. :param template: The template to use to set attributes on the span.
  996. :param send_pii: Whether to send PII data.
  997. :param result: The result of the wrapped function.
  998. """
  999. span.update_data(_get_output_attributes(template, send_pii, result) or {})
  1000. def _should_continue_trace(baggage: "Optional[Baggage]") -> bool:
  1001. """
  1002. Check if we should continue the incoming trace according to the strict_trace_continuation spec.
  1003. https://develop.sentry.dev/sdk/telemetry/traces/#stricttracecontinuation
  1004. """
  1005. client = sentry_sdk.get_client()
  1006. parsed_dsn = client.parsed_dsn
  1007. client_org_id = parsed_dsn.org_id if parsed_dsn else None
  1008. baggage_org_id = baggage.sentry_items.get("org_id") if baggage else None
  1009. if (
  1010. client_org_id is not None
  1011. and baggage_org_id is not None
  1012. and client_org_id != baggage_org_id
  1013. ):
  1014. logger.debug(
  1015. f"Starting a new trace because org IDs don't match (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})"
  1016. )
  1017. return False
  1018. strict_trace_continuation: bool = client.options.get(
  1019. "strict_trace_continuation", False
  1020. )
  1021. if strict_trace_continuation:
  1022. if (baggage_org_id is not None and client_org_id is None) or (
  1023. baggage_org_id is None and client_org_id is not None
  1024. ):
  1025. logger.debug(
  1026. f"Starting a new trace because strict trace continuation is enabled and one org ID is missing (incoming baggage org_id: {baggage_org_id}, SDK org_id: {client_org_id})"
  1027. )
  1028. return False
  1029. return True
  1030. def add_sentry_baggage_to_headers(
  1031. headers: "MutableMapping[str, str]", sentry_baggage: str
  1032. ) -> None:
  1033. """Add the Sentry baggage to the headers.
  1034. This function directly mutates the provided headers. The provided sentry_baggage
  1035. is appended to the existing baggage. If the baggage already contains Sentry items,
  1036. they are stripped out first.
  1037. """
  1038. existing_baggage = headers.get(BAGGAGE_HEADER_NAME, "")
  1039. stripped_existing_baggage = Baggage.strip_sentry_baggage(existing_baggage)
  1040. separator = "," if len(stripped_existing_baggage) > 0 else ""
  1041. headers[BAGGAGE_HEADER_NAME] = (
  1042. stripped_existing_baggage + separator + sentry_baggage
  1043. )
  1044. # Circular imports
  1045. from sentry_sdk.tracing import (
  1046. BAGGAGE_HEADER_NAME,
  1047. LOW_QUALITY_TRANSACTION_SOURCES,
  1048. SENTRY_TRACE_HEADER_NAME,
  1049. )
  1050. if TYPE_CHECKING:
  1051. from sentry_sdk.tracing import Span