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

1import re 

2import contextlib 

3import json 

4import math 

5 

6from numbers import Real 

7 

8import sentry_sdk 

9 

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 

21 

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 

28 

29if MYPY: 29 ↛ 30line 29 didn't jump to line 30, because the condition on line 29 was never true

30 import typing 

31 

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 

37 

38 

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) 

46 

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) 

57 

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) 

66 

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) 

78 

79 

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 

89 

90 def __getitem__(self, key): 

91 # type: (str) -> Optional[Any] 

92 return self.environ[self.prefix + key.replace("-", "_").upper()] 

93 

94 def __len__(self): 

95 # type: () -> int 

96 return sum(1 for _ in iter(self)) 

97 

98 def __iter__(self): 

99 # type: () -> Generator[str, None, None] 

100 for k in self.environ: 

101 if not isinstance(k, str): 

102 continue 

103 

104 k = k.replace("-", "_").upper() 

105 if not k.startswith(self.prefix): 

106 continue 

107 

108 yield k[len(self.prefix) :] 

109 

110 

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 """ 

117 

118 return bool( 

119 options.get("traces_sample_rate") is not None 

120 or options.get("traces_sampler") is not None 

121 ) 

122 

123 

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 """ 

130 

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 

141 

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 

151 

152 return True 

153 

154 

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] 

165 

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 

172 

173 if paramstyle == "pyformat": 

174 paramstyle = "format" 

175 else: 

176 params_list = None 

177 paramstyle = None 

178 

179 query = _format_sql(cursor, query) 

180 

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 

188 

189 with capture_internal_exceptions(): 

190 hub.add_breadcrumb(message=query, category="query", data=data) 

191 

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 

196 

197 

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 ) 

213 

214 

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 

222 

223 if header.startswith("00-") and header.endswith("-00"): 

224 header = header[3:-3] 

225 

226 match = SENTRY_TRACE_REGEX.match(header) 

227 if not match: 

228 return None 

229 

230 trace_id, parent_span_id, sampled_str = match.groups() 

231 parent_sampled = None 

232 

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" 

239 

240 return { 

241 "trace_id": trace_id, 

242 "parent_span_id": parent_span_id, 

243 "parent_sampled": parent_sampled, 

244 } 

245 

246 

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 = "" 

255 

256 if header: 

257 # find sentry's entry, if any 

258 sentry_match = SENTRY_TRACESTATE_ENTRY_REGEX.search(header) 

259 

260 if sentry_match: 

261 sentry_entry = sentry_match.group(1) 

262 

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)) 

266 

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 

275 

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 ) 

280 

281 return { 

282 "sentry_tracestate": sentry_entry, 

283 "third_party_tracestate": third_party_entry, 

284 } 

285 

286 

287def compute_tracestate_value(data): 

288 # type: (typing.Mapping[str, str]) -> str 

289 """ 

290 Computes a new tracestate value using the given data. 

291 

292 Note: Returns just the base64-encoded data, NOT the full `sentry=...` 

293 tracestate entry. 

294 """ 

295 

296 tracestate_json = json.dumps(data, default=safe_str) 

297 

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("=") 

304 

305 

306def compute_tracestate_entry(span): 

307 # type: (Span) -> Optional[str] 

308 """ 

309 Computes a new sentry tracestate for the span. Includes the `sentry=`. 

310 

311 Will return `None` if there's no client and/or no DSN. 

312 """ 

313 data = {} 

314 

315 hub = span.hub or sentry_sdk.Hub.current 

316 

317 client = hub.client 

318 scope = hub.scope 

319 

320 if client and client.options.get("dsn"): 

321 options = client.options 

322 user = scope._user 

323 

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 } 

330 

331 if user and (user.get("id") or user.get("segment")): 

332 user_data = {} 

333 

334 if user.get("id"): 

335 user_data["id"] = user["id"] 

336 

337 if user.get("segment"): 

338 user_data["segment"] = user["segment"] 

339 

340 data["user"] = user_data 

341 

342 if span.containing_transaction: 

343 data["transaction"] = span.containing_transaction.name 

344 

345 return "sentry=" + compute_tracestate_value(data) 

346 

347 return None 

348 

349 

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 

357 

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 + "==") 

369 

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 ) 

380 

381 return inflated_tracestate 

382 

383 

384def _format_sql(cursor, sql): 

385 # type: (Any, str) -> Optional[str] 

386 

387 real_sql = None 

388 

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 

400 

401 return real_sql or to_string(sql) 

402 

403 

404def has_tracestate_enabled(span=None): 

405 # type: (Optional[Span]) -> bool 

406 

407 client = ((span and span.hub) or sentry_sdk.Hub.current).client 

408 options = client and client.options 

409 

410 return bool(options and options["_experiments"].get("propagate_tracestate")) 

411 

412 

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")) 

418 

419 

420class Baggage(object): 

421 __slots__ = ("sentry_items", "third_party_items", "mutable") 

422 

423 SENTRY_PREFIX = "sentry-" 

424 SENTRY_PREFIX_REGEX = re.compile("^sentry-") 

425 

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 ] 

437 

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 

447 

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 

457 

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 

470 

471 return Baggage(sentry_items, third_party_items, mutable) 

472 

473 def freeze(self): 

474 # type: () -> None 

475 self.mutable = False 

476 

477 def dynamic_sampling_context(self): 

478 # type: () -> Dict[str, str] 

479 header = {} 

480 

481 for key in Baggage.DSC_KEYS: 

482 item = self.sentry_items.get(key) 

483 if item: 

484 header[key] = item 

485 

486 return header 

487 

488 def serialize(self, include_third_party=False): 

489 # type: (bool) -> str 

490 items = [] 

491 

492 for key, val in iteritems(self.sentry_items): 

493 item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(val) 

494 items.append(item) 

495 

496 if include_third_party: 

497 items.append(self.third_party_items) 

498 

499 return ",".join(items) 

500 

501 

502# Circular imports 

503 

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