|
1 | 1 | import contextlib
|
| 2 | +from decimal import Decimal |
2 | 3 | import inspect
|
3 | 4 | import os
|
4 | 5 | import re
|
5 | 6 | import sys
|
6 | 7 | from collections.abc import Mapping
|
7 | 8 | from datetime import timedelta
|
8 | 9 | from functools import wraps
|
| 10 | +from random import Random |
9 | 11 | from urllib.parse import quote, unquote
|
10 | 12 | import uuid
|
11 | 13 |
|
|
19 | 21 | match_regex_list,
|
20 | 22 | qualname_from_function,
|
21 | 23 | to_string,
|
| 24 | + try_decimal, |
22 | 25 | is_sentry_url,
|
23 | 26 | _is_external_source,
|
24 | 27 | _is_in_project_root,
|
|
45 | 48 | "[ \t]*$" # whitespace
|
46 | 49 | )
|
47 | 50 |
|
| 51 | +_SAMPLE_RAND_PRECISION = 6 |
| 52 | +"""sample_rand values have 6-digit precision""" |
| 53 | + |
48 | 54 | # This is a normal base64 regex, modified to reflect that fact that we strip the
|
49 | 55 | # trailing = or == off
|
50 | 56 | base64_stripped = (
|
@@ -418,13 +424,17 @@ def from_incoming_data(cls, incoming_data):
|
418 | 424 | propagation_context = PropagationContext()
|
419 | 425 | propagation_context.update(sentrytrace_data)
|
420 | 426 |
|
| 427 | + if propagation_context is not None: |
| 428 | + propagation_context._fill_sample_rand() |
| 429 | + |
421 | 430 | return propagation_context
|
422 | 431 |
|
423 | 432 | @property
|
424 | 433 | def trace_id(self):
|
425 | 434 | # type: () -> str
|
426 | 435 | """The trace id of the Sentry trace."""
|
427 | 436 | if not self._trace_id:
|
| 437 | + # New trace, don't fill in sample_rand |
428 | 438 | self._trace_id = uuid.uuid4().hex
|
429 | 439 |
|
430 | 440 | return self._trace_id
|
@@ -469,6 +479,56 @@ def __repr__(self):
|
469 | 479 | self.dynamic_sampling_context,
|
470 | 480 | )
|
471 | 481 |
|
| 482 | + def _fill_sample_rand(self): |
| 483 | + # type: () -> None |
| 484 | + """ |
| 485 | + Ensure that there is a valid sample_rand value in the dynamic_sampling_context. |
| 486 | +
|
| 487 | + If there is a valid sample_rand value in the dynamic_sampling_context, we keep it. |
| 488 | + Otherwise, we generate a sample_rand value according to the following: |
| 489 | +
|
| 490 | + - If we have a parent_sampled value and a sample_rate in the DSC, we compute |
| 491 | + a sample_rand value randomly in the range: |
| 492 | + - [0, sample_rate) if parent_sampled is True, |
| 493 | + - or, in the range [sample_rate, 1) if parent_sampled is False. |
| 494 | +
|
| 495 | + - If either parent_sampled or sample_rate is missing, we generate a random |
| 496 | + value in the range [0, 1). |
| 497 | +
|
| 498 | + The sample_rand is deterministically generated from the trace_id, if present. |
| 499 | +
|
| 500 | + This function does nothing if there is no dynamic_sampling_context. |
| 501 | + """ |
| 502 | + if self.dynamic_sampling_context is None: |
| 503 | + return |
| 504 | + |
| 505 | + sample_rand = try_decimal(self.dynamic_sampling_context.get("sample_rand")) |
| 506 | + if sample_rand is not None and 0 <= sample_rand < 1: |
| 507 | + # sample_rand is present and valid, so don't overwrite it |
| 508 | + return |
| 509 | + |
| 510 | + # Get the sample rate and compute the transformation that will map the random value |
| 511 | + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). |
| 512 | + sample_rate = try_decimal(self.dynamic_sampling_context.get("sample_rate")) |
| 513 | + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) |
| 514 | + |
| 515 | + try: |
| 516 | + sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) |
| 517 | + except ValueError: |
| 518 | + # ValueError is raised if the interval is invalid, i.e. lower >= upper. |
| 519 | + # lower >= upper might happen if the incoming trace's sampled flag |
| 520 | + # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. |
| 521 | + # We cannot generate a sensible sample_rand value in this case. |
| 522 | + logger.debug( |
| 523 | + f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " |
| 524 | + f"and sample_rate={sample_rate}." |
| 525 | + ) |
| 526 | + return |
| 527 | + |
| 528 | + self.dynamic_sampling_context["sample_rand"] = ( |
| 529 | + f"{sample_rand:.{_SAMPLE_RAND_PRECISION}f}" # noqa: E231 |
| 530 | + ) |
| 531 | + |
472 | 532 |
|
473 | 533 | class Baggage:
|
474 | 534 | """
|
@@ -748,6 +808,56 @@ def get_current_span(scope=None):
|
748 | 808 | return current_span
|
749 | 809 |
|
750 | 810 |
|
| 811 | +def _generate_sample_rand( |
| 812 | + trace_id, # type: Optional[str] |
| 813 | + *, |
| 814 | + interval=(Decimal(0), Decimal(1)), # type: tuple[Decimal, Decimal] # noqa: B008 |
| 815 | +): |
| 816 | + # type: (...) -> Decimal |
| 817 | + """Generate a sample_rand value from a trace ID. |
| 818 | +
|
| 819 | + The generated value will be pseudorandomly chosen from the provided |
| 820 | + interval. Specifically, given (lower, upper) = interval, the generated |
| 821 | + value will be in the range [lower, upper). |
| 822 | +
|
| 823 | + The pseudorandom number generator is seeded with the trace ID. |
| 824 | + """ |
| 825 | + lower_decimal, upper_decimal = interval |
| 826 | + if not lower_decimal < upper_decimal: |
| 827 | + raise ValueError("Invalid interval: lower must be less than upper") |
| 828 | + |
| 829 | + # Since sample_rand values have 6-digit precision, we generate the |
| 830 | + # value as an integer in the range [lower_decimal * 10**6, upper_decimal * 10**6), |
| 831 | + # and then scale it to the desired range. |
| 832 | + lower_int = int(lower_decimal.scaleb(_SAMPLE_RAND_PRECISION)) |
| 833 | + upper_int = int(upper_decimal.scaleb(_SAMPLE_RAND_PRECISION)) |
| 834 | + |
| 835 | + if lower_int == upper_int: |
| 836 | + # Edge case: lower_decimal < upper_decimal, but due to int truncation, |
| 837 | + # lower_int == upper_int. In this case, we return |
| 838 | + # lower_int.scaleb(-SCALE_EXPONENT) here, since calling randrange() |
| 839 | + # with the same lower and upper bounds will raise an error. |
| 840 | + return Decimal(lower_int).scaleb(-_SAMPLE_RAND_PRECISION) |
| 841 | + |
| 842 | + value = Random(trace_id).randrange(lower_int, upper_int) |
| 843 | + return Decimal(value).scaleb(-_SAMPLE_RAND_PRECISION) |
| 844 | + |
| 845 | + |
| 846 | +def _sample_rand_range(parent_sampled, sample_rate): |
| 847 | + # type: (Optional[bool], Optional[Decimal]) -> tuple[Decimal, Decimal] |
| 848 | + """ |
| 849 | + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values |
| 850 | + that a generated sample_rand value must fall into, given the parent_sampled and |
| 851 | + sample_rate values. |
| 852 | + """ |
| 853 | + if parent_sampled is None or sample_rate is None: |
| 854 | + return Decimal(0), Decimal(1) |
| 855 | + elif parent_sampled is True: |
| 856 | + return Decimal(0), sample_rate |
| 857 | + else: # parent_sampled is False |
| 858 | + return sample_rate, Decimal(1) |
| 859 | + |
| 860 | + |
751 | 861 | # Circular imports
|
752 | 862 | from sentry_sdk.tracing import (
|
753 | 863 | BAGGAGE_HEADER_NAME,
|
|
0 commit comments