-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(aci): Track DataCondition errors and propagate such results as tainted #103003
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09b605d
2ff5df8
1a84559
cd40800
bdd934c
ed16526
2286b0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ | |
| from sentry.db.models import DefaultFieldsModel, region_silo_model, sane_repr | ||
| from sentry.utils import metrics, registry | ||
| from sentry.workflow_engine.registry import condition_handler_registry | ||
| from sentry.workflow_engine.types import DataConditionResult, DetectorPriorityLevel | ||
| from sentry.workflow_engine.types import ConditionError, DataConditionResult, DetectorPriorityLevel | ||
| from sentry.workflow_engine.utils import scopedstats | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
@@ -145,7 +145,7 @@ def get_snapshot(self) -> dict[str, Any]: | |
| "condition_result": self.condition_result, | ||
| } | ||
|
|
||
| def get_condition_result(self) -> DataConditionResult: | ||
| def get_condition_result(self) -> DataConditionResult | ConditionError: | ||
| match self.condition_result: | ||
| case float() | bool(): | ||
| return self.condition_result | ||
|
|
@@ -159,15 +159,15 @@ def get_condition_result(self) -> DataConditionResult: | |
| "Invalid condition result", | ||
| extra={"condition_result": self.condition_result, "id": self.id}, | ||
| ) | ||
| return ConditionError(msg="Invalid condition result") | ||
|
|
||
| return None | ||
|
|
||
| def _evaluate_operator(self, condition_type: Condition, value: T) -> DataConditionResult: | ||
| def _evaluate_operator( | ||
| self, condition_type: Condition, value: T | ||
| ) -> DataConditionResult | ConditionError: | ||
| # If the condition is a base type, handle it directly | ||
| op = CONDITION_OPS[condition_type] | ||
| result = None | ||
| try: | ||
| result = op(cast(Any, value), self.comparison) | ||
| return op(cast(Any, value), self.comparison) | ||
| except TypeError: | ||
| logger.exception( | ||
| "Invalid comparison for data condition", | ||
|
|
@@ -178,19 +178,20 @@ def _evaluate_operator(self, condition_type: Condition, value: T) -> DataConditi | |
| "condition_id": self.id, | ||
| }, | ||
| ) | ||
|
|
||
| return result | ||
| return ConditionError(msg="Invalid comparison for data condition") | ||
|
|
||
| @scopedstats.timer() | ||
| def _evaluate_condition(self, condition_type: Condition, value: T) -> DataConditionResult: | ||
| def _evaluate_condition( | ||
| self, condition_type: Condition, value: T | ||
| ) -> DataConditionResult | ConditionError: | ||
| try: | ||
| handler = condition_handler_registry.get(condition_type) | ||
| except registry.NoRegistrationExistsError: | ||
| logger.exception( | ||
| "No registration exists for condition", | ||
| extra={"type": self.type, "id": self.id}, | ||
| ) | ||
| return None | ||
| return ConditionError(msg="No registration exists for condition") | ||
|
|
||
| should_be_fast = not is_slow_condition(self) | ||
| start_time = time.time() | ||
|
|
@@ -212,7 +213,7 @@ def _evaluate_condition(self, condition_type: Condition, value: T) -> DataCondit | |
| "error": str(e), | ||
| }, | ||
| ) | ||
| return None | ||
| return ConditionError(msg=str(e)) | ||
| finally: | ||
| duration = time.time() - start_time | ||
| if should_be_fast and duration >= FAST_CONDITION_TOO_SLOW_THRESHOLD.total_seconds(): | ||
|
|
@@ -230,17 +231,17 @@ def _evaluate_condition(self, condition_type: Condition, value: T) -> DataCondit | |
|
|
||
| return result | ||
|
|
||
| def evaluate_value(self, value: T) -> DataConditionResult: | ||
| def evaluate_value(self, value: T) -> DataConditionResult | ConditionError: | ||
| try: | ||
| condition_type = Condition(self.type) | ||
| except ValueError: | ||
| logger.exception( | ||
| "Invalid condition type", | ||
| extra={"type": self.type, "id": self.id}, | ||
| ) | ||
| return None | ||
| return ConditionError(msg="Invalid condition type") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we just raise these as errors, then handle them in the evaluate methods? just thinking it'd be nice to have a single place to be able to change the control flow, not sure how much we'd need to change in the evaluate off the top of my head to support that as a new flow tho.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is an important question, and I'm not settled on it, but I leaned toward return value like this because:
|
||
|
|
||
| result: DataConditionResult | ||
| result: DataConditionResult | ConditionError | ||
| if condition_type in CONDITION_OPS: | ||
| result = self._evaluate_operator(condition_type, value) | ||
| else: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,8 @@ | |
| from sentry.models.owner_base import OwnerModel | ||
| from sentry.workflow_engine.models.data_condition import DataCondition, is_slow_condition | ||
| from sentry.workflow_engine.models.data_condition_group import DataConditionGroup | ||
| from sentry.workflow_engine.types import WorkflowEventData | ||
| from sentry.workflow_engine.processors.data_condition_group import TriggerResult | ||
| from sentry.workflow_engine.types import ConditionError, WorkflowEventData | ||
|
|
||
| from .json_config import JSONConfigBase | ||
|
|
||
|
|
@@ -93,7 +94,7 @@ def get_audit_log_data(self) -> dict[str, Any]: | |
|
|
||
| def evaluate_trigger_conditions( | ||
| self, event_data: WorkflowEventData, when_data_conditions: list[DataCondition] | None = None | ||
| ) -> tuple[bool, list[DataCondition]]: | ||
| ) -> tuple[TriggerResult, list[DataCondition]]: | ||
| """ | ||
| Evaluate the conditions for the workflow trigger and return if the evaluation was successful. | ||
| If there aren't any workflow trigger conditions, the workflow is considered triggered. | ||
|
|
@@ -104,7 +105,7 @@ def evaluate_trigger_conditions( | |
| ) | ||
|
|
||
| if self.when_condition_group_id is None: | ||
| return True, [] | ||
| return TriggerResult.TRUE, [] | ||
|
|
||
| workflow_event_data = replace(event_data, workflow_env=self.environment) | ||
| try: | ||
|
|
@@ -116,7 +117,7 @@ def evaluate_trigger_conditions( | |
| "DataConditionGroup does not exist", | ||
| extra={"id": self.when_condition_group_id}, | ||
| ) | ||
| return False, [] | ||
| return TriggerResult(False, ConditionError(msg="DataConditionGroup does not exist")), [] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. something i was grappling about on my pr for this stuff, should we make an enum for the error messages? so we could do like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this context I'm not really expecting anyone to use the message text, it's more of a comment threaded through at runtime, so in this specific case i prefer it to be loose because we don't gain much by standardization. |
||
| group_evaluation, remaining_conditions = process_data_condition_group( | ||
| group, workflow_event_data, when_data_conditions | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rather than changing the external interface on all of the detectors, could we just have like a
_internal_evaluateor whatevs? (I'm also hoping to get some time to make a proper abstract class and then an implementingDetectorclass that is stateless, #TODO)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is something I've debated.
PEP8 is a bit fuzzy here, but the conventional interpretation seems to be (and I think stdlib shows this) that underscore prefixed means "not part of the interface" but anything we're exposing to subclasses is part of the interface.
So, I went with that. (ChatGPT agreed when asked in neutral language).
I think it's fuzzy, so I'm happy to come back and rename.