Browse Source

ref(dynamic_sampling): Redesign of dynamic sampling rules [TET-566] (#42762)

Riccardo Busetti 2 years ago
parent
commit
28c16f277c

+ 1 - 0
.github/CODEOWNERS

@@ -355,6 +355,7 @@ yarn.lock                                                @getsentry/owners-js-de
 /src/sentry/api/endpoints/organization_sessions.py                               @getsentry/telemetry-experience
 /src/sentry/api/endpoints/project_dynamic_sampling.py                            @getsentry/telemetry-experience
 /src/sentry/api/endpoints/organization_dynamic_sampling_sdk_versions.py          @getsentry/telemetry-experience
+/src/sentry/dynamic_sampling/                                                    @getsentry/telemetry-experience
 /src/sentry/snuba/metrics/query.py                                               @getsentry/telemetry-experience
 /src/sentry/release_health/metrics_sessions_v2.py                                @getsentry/telemetry-experience
 /tests/sentry/api/endpoints/test_organization_metric_data.py                     @getsentry/telemetry-experience

+ 1 - 2
src/sentry/api/endpoints/project_details.py

@@ -23,8 +23,7 @@ from sentry.api.serializers.rest_framework.origin import OriginField
 from sentry.auth.superuser import is_active_superuser
 from sentry.constants import RESERVED_PROJECT_SLUGS
 from sentry.datascrubbing import validate_pii_config_update
-from sentry.dynamic_sampling.rules_generator import generate_rules
-from sentry.dynamic_sampling.utils import get_supported_biases_ids, get_user_biases
+from sentry.dynamic_sampling import generate_rules, get_supported_biases_ids, get_user_biases
 from sentry.grouping.enhancer import Enhancements, InvalidEnhancerConfig
 from sentry.grouping.fingerprinting import FingerprintingRules, InvalidFingerprintingConfig
 from sentry.ingest.inbound_filters import FilterTypes

+ 2 - 1
src/sentry/discover/models.py

@@ -12,7 +12,6 @@ from sentry.db.models import (
 )
 from sentry.db.models.fields import JSONField
 from sentry.db.models.fields.bounded import BoundedBigIntegerField
-from sentry.dynamic_sampling.utils import RuleType, get_enabled_user_biases
 from sentry.models.projectteam import ProjectTeam
 from sentry.tasks.relay import schedule_invalidate_project_config
 
@@ -102,6 +101,8 @@ class TeamKeyTransactionModelManager(BaseManager):
         if features.has("organizations:dynamic-sampling", project.organization) and options.get(
             "dynamic-sampling:enabled-biases"
         ):
+            from sentry.dynamic_sampling import RuleType, get_enabled_user_biases
+
             # check if option is enabled
             enabled_biases = get_enabled_user_biases(
                 project.get_option("sentry:dynamic_sampling_biases", None)

+ 52 - 0
src/sentry/dynamic_sampling/__init__.py

@@ -0,0 +1,52 @@
+from .rules.base import generate_rules
+from .rules.biases.boost_environments_bias import BoostEnvironmentsRulesGenerator
+from .rules.biases.boost_key_transactions_bias import BoostKeyTransactionsRulesGenerator
+from .rules.biases.boost_latest_releases_bias import BoostLatestReleasesRulesGenerator
+from .rules.biases.ignore_health_checks_bias import (
+    HEALTH_CHECK_GLOBS,
+    IgnoreHealthChecksRulesGenerator,
+)
+from .rules.helpers.latest_releases import (
+    ExtendedBoostedRelease,
+    LatestReleaseBias,
+    LatestReleaseParams,
+    ProjectBoostedReleases,
+    get_redis_client_for_ds,
+)
+from .rules.helpers.time_to_adoptions import LATEST_RELEASE_TTAS, Platform
+from .rules.logging import should_log_rules_change
+from .rules.utils import (
+    BOOSTED_KEY_TRANSACTION_LIMIT,
+    DEFAULT_BIASES,
+    RESERVED_IDS,
+    RuleType,
+    get_enabled_user_biases,
+    get_rule_hash,
+    get_supported_biases_ids,
+    get_user_biases,
+)
+
+__all__ = [
+    "generate_rules",
+    "get_supported_biases_ids",
+    "get_user_biases",
+    "get_enabled_user_biases",
+    "get_redis_client_for_ds",
+    "get_rule_hash",
+    "should_log_rules_change",
+    "RuleType",
+    "ExtendedBoostedRelease",
+    "ProjectBoostedReleases",
+    "Platform",
+    "LatestReleaseBias",
+    "LatestReleaseParams",
+    "IgnoreHealthChecksRulesGenerator",
+    "BoostKeyTransactionsRulesGenerator",
+    "BoostEnvironmentsRulesGenerator",
+    "BoostLatestReleasesRulesGenerator",
+    "LATEST_RELEASE_TTAS",
+    "BOOSTED_KEY_TRANSACTION_LIMIT",
+    "HEALTH_CHECK_GLOBS",
+    "RESERVED_IDS",
+    "DEFAULT_BIASES",
+]

+ 0 - 0
src/sentry/dynamic_sampling/rules/__init__.py


+ 62 - 0
src/sentry/dynamic_sampling/rules/base.py

@@ -0,0 +1,62 @@
+from typing import List, OrderedDict, Set
+
+import sentry_sdk
+
+from sentry import quotas
+from sentry.dynamic_sampling.rules.biases.base import Bias, BiasParams
+from sentry.dynamic_sampling.rules.combine import get_relay_biases_combinator
+from sentry.dynamic_sampling.rules.logging import log_rules
+from sentry.dynamic_sampling.rules.utils import BaseRule, RuleType, get_enabled_user_biases
+from sentry.models import Project
+
+ALWAYS_ALLOWED_RULE_TYPES = {RuleType.UNIFORM_RULE}
+
+
+def get_guarded_blended_sample_rate(project: Project) -> float:
+    sample_rate = quotas.get_blended_sample_rate(project)
+
+    if sample_rate is None:
+        raise Exception("get_blended_sample_rate returns none")
+
+    return float(sample_rate)
+
+
+def _get_rules_of_enabled_biases(
+    project: Project,
+    base_sample_rate: float,
+    enabled_biases: Set[str],
+    combined_biases: OrderedDict[RuleType, Bias],
+) -> List[BaseRule]:
+    rules = []
+
+    for (rule_type, bias) in combined_biases.items():
+        # All biases besides the uniform won't be enabled in case we have 100% base sample rate. This has been
+        # done because if we don't have a sample rate < 100%, it doesn't make sense to enable dynamic sampling in
+        # the first place. Technically dynamic sampling it is still enabled but for our customers this detail is
+        # not important.
+        if rule_type in ALWAYS_ALLOWED_RULE_TYPES or (
+            rule_type.value in enabled_biases and base_sample_rate < 1.0
+        ):
+            rules += bias.get_rules(BiasParams(project, base_sample_rate))
+
+    log_rules(project.organization.id, project.id, rules)
+
+    return rules
+
+
+def generate_rules(project: Project) -> List[BaseRule]:
+    try:
+        return _get_rules_of_enabled_biases(
+            project,
+            get_guarded_blended_sample_rate(project),
+            get_enabled_user_biases(project.get_option("sentry:dynamic_sampling_biases", None)),
+            # To add new biases you will need:
+            # * Data provider
+            # * Rules generator
+            # * Bias
+            # check in the dynamic_sampling/rules/biases module how existing biases are implemented.
+            get_relay_biases_combinator().get_combined_biases(),
+        )
+    except Exception as e:
+        sentry_sdk.capture_exception(e)
+        return []

+ 0 - 0
src/sentry/dynamic_sampling/rules/biases/__init__.py


+ 55 - 0
src/sentry/dynamic_sampling/rules/biases/base.py

@@ -0,0 +1,55 @@
+from abc import ABC, abstractmethod
+from collections import namedtuple
+from typing import Any, Dict, List, Type
+
+from sentry.dynamic_sampling.rules.utils import BaseRule
+
+BiasData = Dict[str, Any]
+BiasParams = namedtuple("BiasParams", "project base_sample_rate")
+
+
+class BiasDataProvider(ABC):
+    """
+    Base class representing the provider of data needed to generate a rule connected to a bias.
+    """
+
+    @abstractmethod
+    def get_bias_data(self, bias_params: BiasParams) -> BiasData:
+        raise NotImplementedError
+
+
+class BiasRulesGenerator(ABC):
+    """
+    Base class representing the generator of rules connected to a bias.
+    """
+
+    def __init__(self, data_provider: BiasDataProvider):
+        self.data_provider = data_provider
+
+    def generate_bias_rules(self, bias_params: BiasParams) -> List[BaseRule]:
+        return self._generate_bias_rules(self.data_provider.get_bias_data(bias_params))
+
+    @abstractmethod
+    def _generate_bias_rules(self, bias_data: BiasData) -> List[BaseRule]:
+        raise NotImplementedError
+
+
+class Bias(ABC):
+    """
+    Base class containing business logic that automatically handles the execution flow and inter-dependencies of the
+    data provider and rules generator.
+
+    The goal of this class is to abstract away the interaction between the provider and generator, in order to make
+    this abstractions work like a small framework.
+    """
+
+    def __init__(
+        self,
+        data_provider_cls: Type[BiasDataProvider],
+        rules_generator_cls: Type[BiasRulesGenerator],
+    ):
+        self.data_provider = data_provider_cls()
+        self.rules_generator = rules_generator_cls(self.data_provider)
+
+    def get_rules(self, bias_params: BiasParams) -> List[BaseRule]:
+        return self.rules_generator.generate_bias_rules(bias_params)

+ 43 - 0
src/sentry/dynamic_sampling/rules/biases/boost_environments_bias.py

@@ -0,0 +1,43 @@
+from typing import List
+
+from sentry.dynamic_sampling.rules.biases.base import (
+    Bias,
+    BiasData,
+    BiasDataProvider,
+    BiasParams,
+    BiasRulesGenerator,
+)
+from sentry.dynamic_sampling.rules.utils import RESERVED_IDS, BaseRule, RuleType
+
+
+class BoostEnvironmentsDataProvider(BiasDataProvider):
+    def get_bias_data(self, bias_params: BiasParams) -> BiasData:
+        return {"id": RESERVED_IDS[RuleType.BOOST_ENVIRONMENTS_RULE]}
+
+
+class BoostEnvironmentsRulesGenerator(BiasRulesGenerator):
+    def _generate_bias_rules(self, bias_data: BiasData) -> List[BaseRule]:
+        return [
+            {
+                "sampleRate": 1,
+                "type": "trace",
+                "condition": {
+                    "op": "or",
+                    "inner": [
+                        {
+                            "op": "glob",
+                            "name": "trace.environment",
+                            "value": ["*dev*", "*test*"],
+                            "options": {"ignoreCase": True},
+                        }
+                    ],
+                },
+                "active": True,
+                "id": bias_data["id"],
+            }
+        ]
+
+
+class BoostEnvironmentsBias(Bias):
+    def __init__(self) -> None:
+        super().__init__(BoostEnvironmentsDataProvider, BoostEnvironmentsRulesGenerator)

+ 56 - 0
src/sentry/dynamic_sampling/rules/biases/boost_key_transactions_bias.py

@@ -0,0 +1,56 @@
+from typing import List
+
+from sentry.dynamic_sampling.rules.biases.base import (
+    Bias,
+    BiasData,
+    BiasDataProvider,
+    BiasParams,
+    BiasRulesGenerator,
+)
+from sentry.dynamic_sampling.rules.helpers.key_transactions import get_key_transactions
+from sentry.dynamic_sampling.rules.utils import (
+    KEY_TRANSACTION_BOOST_FACTOR,
+    RESERVED_IDS,
+    BaseRule,
+    RuleType,
+)
+
+
+class BoostKeyTransactionsDataProvider(BiasDataProvider):
+    def get_bias_data(self, bias_params: BiasParams) -> BiasData:
+        return {
+            "id": RESERVED_IDS[RuleType.BOOST_KEY_TRANSACTIONS_RULE],
+            "sampleRate": min(1.0, bias_params.base_sample_rate * KEY_TRANSACTION_BOOST_FACTOR),
+            "keyTransactions": get_key_transactions(bias_params.project),
+        }
+
+
+class BoostKeyTransactionsRulesGenerator(BiasRulesGenerator):
+    def _generate_bias_rules(self, bias_data: BiasData) -> List[BaseRule]:
+        if len(bias_data["keyTransactions"]) == 0:
+            return []
+
+        return [
+            {
+                "sampleRate": bias_data["sampleRate"],
+                "type": "transaction",
+                "condition": {
+                    "op": "or",
+                    "inner": [
+                        {
+                            "op": "eq",
+                            "name": "event.transaction",
+                            "value": bias_data["keyTransactions"],
+                            "options": {"ignoreCase": True},
+                        }
+                    ],
+                },
+                "active": True,
+                "id": bias_data["id"],
+            }
+        ]
+
+
+class BoostKeyTransactionsBias(Bias):
+    def __init__(self) -> None:
+        super().__init__(BoostKeyTransactionsDataProvider, BoostKeyTransactionsRulesGenerator)

Some files were not shown because too many files changed in this diff