Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/tracing_utils.py: 31%
210 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import re
2import contextlib
3import json
4import math
6from numbers import Real
8import sentry_sdk
10from sentry_sdk.utils import (
11 capture_internal_exceptions,
12 Dsn,
13 logger,
14 safe_str,
15 to_base64,
16 to_string,
17 from_base64,
18)
19from sentry_sdk._compat import PY2, iteritems
20from sentry_sdk._types import MYPY
22if PY2: 22 ↛ 23line 22 didn't jump to line 23, because the condition on line 22 was never true
23 from collections import Mapping
24 from urllib import quote, unquote
25else:
26 from collections.abc import Mapping
27 from urllib.parse import quote, unquote
29if MYPY: 29 ↛ 30line 29 didn't jump to line 30, because the condition on line 29 was never true
30 import typing
32 from typing import Generator
33 from typing import Optional
34 from typing import Any
35 from typing import Dict
36 from typing import Union
39SENTRY_TRACE_REGEX = re.compile(
40 "^[ \t]*" # whitespace
41 "([0-9a-f]{32})?" # trace_id
42 "-?([0-9a-f]{16})?" # span_id
43 "-?([01])?" # sampled
44 "[ \t]*$" # whitespace
45)
47# This is a normal base64 regex, modified to reflect that fact that we strip the
48# trailing = or == off
49base64_stripped = (
50 # any of the characters in the base64 "alphabet", in multiples of 4
51 "([a-zA-Z0-9+/]{4})*"
52 # either nothing or 2 or 3 base64-alphabet characters (see
53 # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding for
54 # why there's never only 1 extra character)
55 "([a-zA-Z0-9+/]{2,3})?"
56)
58# comma-delimited list of entries of the form `xxx=yyy`
59tracestate_entry = "[^=]+=[^=]+"
60TRACESTATE_ENTRIES_REGEX = re.compile(
61 # one or more xxxxx=yyyy entries
62 "^({te})+"
63 # each entry except the last must be followed by a comma
64 "(,|$)".format(te=tracestate_entry)
65)
67# this doesn't check that the value is valid, just that there's something there
68# of the form `sentry=xxxx`
69SENTRY_TRACESTATE_ENTRY_REGEX = re.compile(
70 # either sentry is the first entry or there's stuff immediately before it,
71 # ending in a comma (this prevents matching something like `coolsentry=xxx`)
72 "(?:^|.+,)"
73 # sentry's part, not including the potential comma
74 "(sentry=[^,]*)"
75 # either there's a comma and another vendor's entry or we end
76 "(?:,.+|$)"
77)
80class EnvironHeaders(Mapping): # type: ignore
81 def __init__(
82 self,
83 environ, # type: typing.Mapping[str, str]
84 prefix="HTTP_", # type: str
85 ):
86 # type: (...) -> None
87 self.environ = environ
88 self.prefix = prefix
90 def __getitem__(self, key):
91 # type: (str) -> Optional[Any]
92 return self.environ[self.prefix + key.replace("-", "_").upper()]
94 def __len__(self):
95 # type: () -> int
96 return sum(1 for _ in iter(self))
98 def __iter__(self):
99 # type: () -> Generator[str, None, None]
100 for k in self.environ:
101 if not isinstance(k, str):
102 continue
104 k = k.replace("-", "_").upper()
105 if not k.startswith(self.prefix):
106 continue
108 yield k[len(self.prefix) :]
111def has_tracing_enabled(options):
112 # type: (Dict[str, Any]) -> bool
113 """
114 Returns True if either traces_sample_rate or traces_sampler is
115 defined, False otherwise.
116 """
118 return bool(
119 options.get("traces_sample_rate") is not None
120 or options.get("traces_sampler") is not None
121 )
124def is_valid_sample_rate(rate):
125 # type: (Any) -> bool
126 """
127 Checks the given sample rate to make sure it is valid type and value (a
128 boolean or a number between 0 and 1, inclusive).
129 """
131 # both booleans and NaN are instances of Real, so a) checking for Real
132 # checks for the possibility of a boolean also, and b) we have to check
133 # separately for NaN
134 if not isinstance(rate, Real) or math.isnan(rate):
135 logger.warning(
136 "[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
137 rate=rate, type=type(rate)
138 )
139 )
140 return False
142 # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
143 rate = float(rate)
144 if rate < 0 or rate > 1:
145 logger.warning(
146 "[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
147 rate=rate
148 )
149 )
150 return False
152 return True
155@contextlib.contextmanager
156def record_sql_queries(
157 hub, # type: sentry_sdk.Hub
158 cursor, # type: Any
159 query, # type: Any
160 params_list, # type: Any
161 paramstyle, # type: Optional[str]
162 executemany, # type: bool
163):
164 # type: (...) -> Generator[Span, None, None]
166 # TODO: Bring back capturing of params by default
167 if hub.client and hub.client.options["_experiments"].get( 167 ↛ 170line 167 didn't jump to line 170, because the condition on line 167 was never true
168 "record_sql_params", False
169 ):
170 if not params_list or params_list == [None]:
171 params_list = None
173 if paramstyle == "pyformat":
174 paramstyle = "format"
175 else:
176 params_list = None
177 paramstyle = None
179 query = _format_sql(cursor, query)
181 data = {}
182 if params_list is not None: 182 ↛ 183line 182 didn't jump to line 183, because the condition on line 182 was never true
183 data["db.params"] = params_list
184 if paramstyle is not None: 184 ↛ 185line 184 didn't jump to line 185, because the condition on line 184 was never true
185 data["db.paramstyle"] = paramstyle
186 if executemany: 186 ↛ 187line 186 didn't jump to line 187, because the condition on line 186 was never true
187 data["db.executemany"] = True
189 with capture_internal_exceptions():
190 hub.add_breadcrumb(message=query, category="query", data=data)
192 with hub.start_span(op="db", description=query) as span:
193 for k, v in data.items(): 193 ↛ 194line 193 didn't jump to line 194, because the loop on line 193 never started
194 span.set_data(k, v)
195 yield span
198def maybe_create_breadcrumbs_from_span(hub, span):
199 # type: (sentry_sdk.Hub, Span) -> None
200 if span.op == "redis": 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true
201 hub.add_breadcrumb(
202 message=span.description, type="redis", category="redis", data=span._tags
203 )
204 elif span.op == "http":
205 hub.add_breadcrumb(type="http", category="httplib", data=span._data)
206 elif span.op == "subprocess":
207 hub.add_breadcrumb(
208 type="subprocess",
209 category="subprocess",
210 message=span.description,
211 data=span._data,
212 )
215def extract_sentrytrace_data(header):
216 # type: (Optional[str]) -> Optional[typing.Mapping[str, Union[str, bool, None]]]
217 """
218 Given a `sentry-trace` header string, return a dictionary of data.
219 """
220 if not header:
221 return None
223 if header.startswith("00-") and header.endswith("-00"):
224 header = header[3:-3]
226 match = SENTRY_TRACE_REGEX.match(header)
227 if not match:
228 return None
230 trace_id, parent_span_id, sampled_str = match.groups()
231 parent_sampled = None
233 if trace_id:
234 trace_id = "{:032x}".format(int(trace_id, 16))
235 if parent_span_id:
236 parent_span_id = "{:016x}".format(int(parent_span_id, 16))
237 if sampled_str:
238 parent_sampled = sampled_str != "0"
240 return {
241 "trace_id": trace_id,
242 "parent_span_id": parent_span_id,
243 "parent_sampled": parent_sampled,
244 }
247def extract_tracestate_data(header):
248 # type: (Optional[str]) -> typing.Mapping[str, Optional[str]]
249 """
250 Extracts the sentry tracestate value and any third-party data from the given
251 tracestate header, returning a dictionary of data.
252 """
253 sentry_entry = third_party_entry = None
254 before = after = ""
256 if header:
257 # find sentry's entry, if any
258 sentry_match = SENTRY_TRACESTATE_ENTRY_REGEX.search(header)
260 if sentry_match:
261 sentry_entry = sentry_match.group(1)
263 # remove the commas after the split so we don't end up with
264 # `xxx=yyy,,zzz=qqq` (double commas) when we put them back together
265 before, after = map(lambda s: s.strip(","), header.split(sentry_entry))
267 # extract sentry's value from its entry and test to make sure it's
268 # valid; if it isn't, discard the entire entry so that a new one
269 # will be created
270 sentry_value = sentry_entry.replace("sentry=", "")
271 if not re.search("^{b64}$".format(b64=base64_stripped), sentry_value):
272 sentry_entry = None
273 else:
274 after = header
276 # if either part is invalid or empty, remove it before gluing them together
277 third_party_entry = (
278 ",".join(filter(TRACESTATE_ENTRIES_REGEX.search, [before, after])) or None
279 )
281 return {
282 "sentry_tracestate": sentry_entry,
283 "third_party_tracestate": third_party_entry,
284 }
287def compute_tracestate_value(data):
288 # type: (typing.Mapping[str, str]) -> str
289 """
290 Computes a new tracestate value using the given data.
292 Note: Returns just the base64-encoded data, NOT the full `sentry=...`
293 tracestate entry.
294 """
296 tracestate_json = json.dumps(data, default=safe_str)
298 # Base64-encoded strings always come out with a length which is a multiple
299 # of 4. In order to achieve this, the end is padded with one or more `=`
300 # signs. Because the tracestate standard calls for using `=` signs between
301 # vendor name and value (`sentry=xxx,dogsaregreat=yyy`), to avoid confusion
302 # we strip the `=`
303 return (to_base64(tracestate_json) or "").rstrip("=")
306def compute_tracestate_entry(span):
307 # type: (Span) -> Optional[str]
308 """
309 Computes a new sentry tracestate for the span. Includes the `sentry=`.
311 Will return `None` if there's no client and/or no DSN.
312 """
313 data = {}
315 hub = span.hub or sentry_sdk.Hub.current
317 client = hub.client
318 scope = hub.scope
320 if client and client.options.get("dsn"):
321 options = client.options
322 user = scope._user
324 data = {
325 "trace_id": span.trace_id,
326 "environment": options["environment"],
327 "release": options.get("release"),
328 "public_key": Dsn(options["dsn"]).public_key,
329 }
331 if user and (user.get("id") or user.get("segment")):
332 user_data = {}
334 if user.get("id"):
335 user_data["id"] = user["id"]
337 if user.get("segment"):
338 user_data["segment"] = user["segment"]
340 data["user"] = user_data
342 if span.containing_transaction:
343 data["transaction"] = span.containing_transaction.name
345 return "sentry=" + compute_tracestate_value(data)
347 return None
350def reinflate_tracestate(encoded_tracestate):
351 # type: (str) -> typing.Optional[Mapping[str, str]]
352 """
353 Given a sentry tracestate value in its encoded form, translate it back into
354 a dictionary of data.
355 """
356 inflated_tracestate = None
358 if encoded_tracestate:
359 # Base64-encoded strings always come out with a length which is a
360 # multiple of 4. In order to achieve this, the end is padded with one or
361 # more `=` signs. Because the tracestate standard calls for using `=`
362 # signs between vendor name and value (`sentry=xxx,dogsaregreat=yyy`),
363 # to avoid confusion we strip the `=` when the data is initially
364 # encoded. Python's decoding function requires they be put back.
365 # Fortunately, it doesn't complain if there are too many, so we just
366 # attach two `=` on spec (there will never be more than 2, see
367 # https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding).
368 tracestate_json = from_base64(encoded_tracestate + "==")
370 try:
371 assert tracestate_json is not None
372 inflated_tracestate = json.loads(tracestate_json)
373 except Exception as err:
374 logger.warning(
375 (
376 "Unable to attach tracestate data to envelope header: {err}"
377 + "\nTracestate value is {encoded_tracestate}"
378 ).format(err=err, encoded_tracestate=encoded_tracestate),
379 )
381 return inflated_tracestate
384def _format_sql(cursor, sql):
385 # type: (Any, str) -> Optional[str]
387 real_sql = None
389 # If we're using psycopg2, it could be that we're
390 # looking at a query that uses Composed objects. Use psycopg2's mogrify
391 # function to format the query. We lose per-parameter trimming but gain
392 # accuracy in formatting.
393 try:
394 if hasattr(cursor, "mogrify"): 394 ↛ 401line 394 didn't jump to line 401, because the condition on line 394 was never false
395 real_sql = cursor.mogrify(sql)
396 if isinstance(real_sql, bytes): 396 ↛ 401line 396 didn't jump to line 401, because the condition on line 396 was never false
397 real_sql = real_sql.decode(cursor.connection.encoding)
398 except Exception:
399 real_sql = None
401 return real_sql or to_string(sql)
404def has_tracestate_enabled(span=None):
405 # type: (Optional[Span]) -> bool
407 client = ((span and span.hub) or sentry_sdk.Hub.current).client
408 options = client and client.options
410 return bool(options and options["_experiments"].get("propagate_tracestate"))
413def has_custom_measurements_enabled():
414 # type: () -> bool
415 client = sentry_sdk.Hub.current.client
416 options = client and client.options
417 return bool(options and options["_experiments"].get("custom_measurements"))
420class Baggage(object):
421 __slots__ = ("sentry_items", "third_party_items", "mutable")
423 SENTRY_PREFIX = "sentry-"
424 SENTRY_PREFIX_REGEX = re.compile("^sentry-")
426 # DynamicSamplingContext
427 DSC_KEYS = [
428 "trace_id",
429 "public_key",
430 "sample_rate",
431 "release",
432 "environment",
433 "transaction",
434 "user_id",
435 "user_segment",
436 ]
438 def __init__(
439 self,
440 sentry_items, # type: Dict[str, str]
441 third_party_items="", # type: str
442 mutable=True, # type: bool
443 ):
444 self.sentry_items = sentry_items
445 self.third_party_items = third_party_items
446 self.mutable = mutable
448 @classmethod
449 def from_incoming_header(cls, header):
450 # type: (Optional[str]) -> Baggage
451 """
452 freeze if incoming header already has sentry baggage
453 """
454 sentry_items = {}
455 third_party_items = ""
456 mutable = True
458 if header:
459 for item in header.split(","):
460 if "=" not in item:
461 continue
462 item = item.strip()
463 key, val = item.split("=")
464 if Baggage.SENTRY_PREFIX_REGEX.match(key):
465 baggage_key = unquote(key.split("-")[1])
466 sentry_items[baggage_key] = unquote(val)
467 mutable = False
468 else:
469 third_party_items += ("," if third_party_items else "") + item
471 return Baggage(sentry_items, third_party_items, mutable)
473 def freeze(self):
474 # type: () -> None
475 self.mutable = False
477 def dynamic_sampling_context(self):
478 # type: () -> Dict[str, str]
479 header = {}
481 for key in Baggage.DSC_KEYS:
482 item = self.sentry_items.get(key)
483 if item:
484 header[key] = item
486 return header
488 def serialize(self, include_third_party=False):
489 # type: (bool) -> str
490 items = []
492 for key, val in iteritems(self.sentry_items):
493 item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(val)
494 items.append(item)
496 if include_third_party:
497 items.append(self.third_party_items)
499 return ",".join(items)
502# Circular imports
504if MYPY: 504 ↛ 505line 504 didn't jump to line 505, because the condition on line 504 was never true
505 from sentry_sdk.tracing import Span