Skip to content

Commit 62b1f73

Browse files
mifu67ceorourke
andauthored
feat(aci milestone 3): delete data in Seer when AD detector is deleted (#101843)
When a detector is deleted, we need to delete the rule data in Seer. Similar implementation to the legacy alert rule code, but uses detector ID instead of alert rule ID. Note that because we are no longer able to use the post delete signal (the `QuerySubscription` model is deleted by the time we get to post delete so we cannot access the `source_id` needed) we cannot ensure Seer knows to delete data on their end during a cascade deletion - however Seer will delete data that doesn't receive an update for 90 days so it will eventually be cleaned up. --------- Co-authored-by: Colleen O'Rourke <[email protected]>
1 parent 7222cf4 commit 62b1f73

File tree

7 files changed

+146
-87
lines changed

7 files changed

+146
-87
lines changed

src/sentry/incidents/logic.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,30 @@ def nullify_id(model: Model) -> None:
755755
)
756756

757757

758+
def delete_anomaly_detection_rule(snuba_query: SnubaQuery, alert_rule: AlertRule) -> None:
759+
"""
760+
Delete accompanying data in Seer for anomaly detection rules
761+
"""
762+
try:
763+
source_id = QuerySubscription.objects.get(snuba_query_id=snuba_query.id).id
764+
success = delete_rule_in_seer(
765+
organization=alert_rule.organization,
766+
source_id=source_id,
767+
)
768+
if not success:
769+
logger.error(
770+
"Call to delete rule data in Seer failed",
771+
extra={
772+
"source_id": source_id,
773+
},
774+
)
775+
except QuerySubscription.DoesNotExist:
776+
logger.exception(
777+
"Snuba query missing query subscription",
778+
extra={"snuba_query_id": snuba_query.id},
779+
)
780+
781+
758782
def update_alert_rule(
759783
alert_rule: AlertRule,
760784
query_type: SnubaQuery.Type | None = None,
@@ -921,18 +945,8 @@ def update_alert_rule(
921945
alert_rule, project, snuba_query, updated_fields, updated_query_fields
922946
)
923947
else:
924-
# if this was a dynamic rule, delete the data in Seer
925948
if alert_rule.detection_type == AlertRuleDetectionType.DYNAMIC:
926-
success = delete_rule_in_seer(
927-
alert_rule=alert_rule,
928-
)
929-
if not success:
930-
logger.error(
931-
"Call to delete rule data in Seer failed",
932-
extra={
933-
"rule_id": alert_rule.id,
934-
},
935-
)
949+
delete_anomaly_detection_rule(snuba_query, alert_rule)
936950
# if this alert was previously a dynamic alert, then we should update the rule to be ready
937951
if alert_rule.status == AlertRuleStatus.NOT_ENOUGH_DATA.value:
938952
alert_rule.update(status=AlertRuleStatus.PENDING.value)
@@ -1088,20 +1102,11 @@ def delete_alert_rule(
10881102
)
10891103
subscriptions = _unpack_snuba_query(alert_rule).subscriptions.all()
10901104

1105+
if alert_rule.detection_type == AlertRuleDetectionType.DYNAMIC:
1106+
delete_anomaly_detection_rule(alert_rule.snuba_query, alert_rule)
1107+
10911108
incidents = Incident.objects.filter(alert_rule=alert_rule)
10921109
if incidents.exists():
1093-
# if this was a dynamic rule, delete the data in Seer
1094-
if alert_rule.detection_type == AlertRuleDetectionType.DYNAMIC:
1095-
success = delete_rule_in_seer(
1096-
alert_rule=alert_rule,
1097-
)
1098-
if not success:
1099-
logger.error(
1100-
"Call to delete rule data in Seer failed",
1101-
extra={
1102-
"rule_id": alert_rule.id,
1103-
},
1104-
)
11051110
AlertRuleActivity.objects.create(
11061111
alert_rule=alert_rule,
11071112
user_id=user.id if user else None,

src/sentry/incidents/metric_issue_detector.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sentry.incidents.logic import enable_disable_subscriptions
99
from sentry.incidents.models.alert_rule import AlertRuleDetectionType
1010
from sentry.relay.config.metric_extraction import on_demand_metrics_feature_flags
11+
from sentry.seer.anomaly_detection.delete_rule import delete_data_in_seer_for_detector
1112
from sentry.seer.anomaly_detection.store_data_workflow_engine import send_new_detector_data
1213
from sentry.snuba.dataset import Dataset
1314
from sentry.snuba.metrics.extraction import should_use_on_demand_metrics
@@ -332,3 +333,11 @@ def create(self, validated_data: dict[str, Any]):
332333

333334
schedule_update_project_config(detector)
334335
return detector
336+
337+
def delete(self):
338+
# Let Seer know we're deleting a dynamic detector so the data can be deleted there too
339+
assert self.instance is not None
340+
detector: Detector = self.instance
341+
delete_data_in_seer_for_detector(detector)
342+
343+
super().delete()

src/sentry/incidents/models/alert_rule.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
ActionService,
3737
ActionTarget,
3838
)
39-
from sentry.seer.anomaly_detection.delete_rule import delete_rule_in_seer
4039
from sentry.snuba.models import QuerySubscription
4140
from sentry.types.actor import Actor
4241
from sentry.utils import metrics
@@ -140,18 +139,6 @@ def clear_alert_rule_subscription_caches(cls, instance: AlertRule, **kwargs: Any
140139
for sub_id in subscription_ids
141140
)
142141

143-
@classmethod
144-
def delete_data_in_seer(cls, instance: AlertRule, **kwargs: Any) -> None:
145-
if instance.detection_type == AlertRuleDetectionType.DYNAMIC:
146-
success = delete_rule_in_seer(alert_rule=instance)
147-
if not success:
148-
logger.error(
149-
"Call to delete rule data in Seer failed",
150-
extra={
151-
"rule_id": instance.id,
152-
},
153-
)
154-
155142

156143
@region_silo_model
157144
class AlertRuleProjects(Model):
@@ -624,7 +611,6 @@ class Meta:
624611

625612

626613
post_delete.connect(AlertRuleManager.clear_subscription_cache, sender=QuerySubscription)
627-
post_delete.connect(AlertRuleManager.delete_data_in_seer, sender=AlertRule)
628614
post_save.connect(AlertRuleManager.clear_subscription_cache, sender=QuerySubscription)
629615
post_save.connect(AlertRuleManager.clear_alert_rule_subscription_caches, sender=AlertRule)
630616
post_delete.connect(AlertRuleManager.clear_alert_rule_subscription_caches, sender=AlertRule)

src/sentry/seer/anomaly_detection/delete_rule.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
from __future__ import annotations
2+
13
import logging
24
from typing import TYPE_CHECKING
35

46
from django.conf import settings
57
from urllib3.exceptions import MaxRetryError, TimeoutError
68

79
from sentry.conf.server import SEER_ALERT_DELETION_URL
10+
from sentry.models.organization import Organization
811
from sentry.net.http import connection_from_url
9-
from sentry.seer.anomaly_detection.types import DeleteAlertDataRequest
12+
from sentry.seer.anomaly_detection.types import AlertInSeer, DataSourceType, DeleteAlertDataRequest
1013
from sentry.seer.signed_seer_api import make_signed_seer_api_request
1114
from sentry.utils import json
1215
from sentry.utils.json import JSONDecodeError
@@ -18,21 +21,51 @@
1821
)
1922

2023
if TYPE_CHECKING:
21-
from sentry.incidents.models.alert_rule import AlertRule
24+
from sentry.workflow_engine.models.detector import Detector
25+
2226

27+
def delete_data_in_seer_for_detector(detector: Detector):
28+
from sentry.incidents.models.alert_rule import AlertRuleDetectionType
29+
from sentry.workflow_engine.models import DataSourceDetector
2330

24-
def delete_rule_in_seer(alert_rule: "AlertRule") -> bool:
31+
data_source_detector = DataSourceDetector.objects.filter(detector_id=detector.id).first()
32+
if not data_source_detector:
33+
logger.error(
34+
"No data source found for detector",
35+
extra={
36+
"detector_id": detector.id,
37+
},
38+
)
39+
return
40+
41+
organization = detector.project.organization
42+
43+
if detector.config.get("detection_type") == AlertRuleDetectionType.DYNAMIC:
44+
success = delete_rule_in_seer(
45+
source_id=int(data_source_detector.data_source.source_id), organization=organization
46+
)
47+
if not success:
48+
logger.error(
49+
"Call to delete rule data in Seer failed",
50+
extra={
51+
"detector_id": detector.id,
52+
},
53+
)
54+
55+
56+
def delete_rule_in_seer(source_id: int, organization: Organization) -> bool:
2557
"""
2658
Send a request to delete an alert rule from Seer. Returns True if the request was successful.
2759
"""
2860
body = DeleteAlertDataRequest(
29-
organization_id=alert_rule.organization.id,
30-
alert={"id": alert_rule.id},
61+
organization_id=organization.id,
62+
alert=AlertInSeer(
63+
id=None, source_id=source_id, source_type=DataSourceType.SNUBA_QUERY_SUBSCRIPTION
64+
),
3165
)
3266
extra_data = {
33-
"rule_id": alert_rule.id,
67+
"source_id": source_id,
3468
}
35-
3669
try:
3770
response = make_signed_seer_api_request(
3871
connection_pool=seer_anomaly_detection_connection_pool,
@@ -46,7 +79,7 @@ def delete_rule_in_seer(alert_rule: "AlertRule") -> bool:
4679
)
4780
return False
4881

49-
if response.status > 400:
82+
if response.status >= 400:
5083
logger.error(
5184
"Error when hitting Seer delete rule data endpoint",
5285
extra={
@@ -75,7 +108,14 @@ def delete_rule_in_seer(alert_rule: "AlertRule") -> bool:
75108
return False
76109

77110
status = results.get("success")
78-
if status is None or status is not True:
111+
if status is None:
112+
logger.error(
113+
"Request to delete alert rule from Seer was unsuccessful",
114+
extra=extra_data,
115+
)
116+
return False
117+
elif status is not True:
118+
extra_data["message"] = results.get("message")
79119
logger.error(
80120
"Request to delete alert rule from Seer was unsuccessful",
81121
extra=extra_data,

tests/sentry/deletions/test_alert_rule.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,14 @@ def test_simple(self) -> None:
7777
assert not AlertRuleWorkflow.objects.filter(alert_rule_id=alert_rule.id).exists()
7878

7979
@with_feature("organizations:anomaly-detection-alerts")
80-
@patch(
81-
"sentry.seer.anomaly_detection.delete_rule.seer_anomaly_detection_connection_pool.urlopen"
82-
)
8380
@patch(
8481
"sentry.seer.anomaly_detection.store_data.seer_anomaly_detection_connection_pool.urlopen"
8582
)
86-
def test_dynamic_alert_rule(
87-
self, mock_store_request: MagicMock, mock_delete_request: MagicMock
88-
) -> None:
83+
def test_dynamic_alert_rule(self, mock_store_request: MagicMock) -> None:
8984
organization = self.create_organization()
9085

9186
seer_return_value = {"success": True}
9287
mock_store_request.return_value = HTTPResponse(orjson.dumps(seer_return_value), status=200)
93-
mock_delete_request.return_value = HTTPResponse(orjson.dumps(seer_return_value), status=200)
9488

9589
alert_rule = self.create_alert_rule(
9690
sensitivity=AlertRuleSensitivity.HIGH,
@@ -120,5 +114,3 @@ def test_dynamic_alert_rule(
120114
assert not AlertRule.objects.filter(id=alert_rule.id).exists()
121115
assert not AlertRuleTrigger.objects.filter(id=alert_rule_trigger.id).exists()
122116
assert not Incident.objects.filter(id=incident.id).exists()
123-
124-
assert mock_delete_request.call_count == 1

0 commit comments

Comments
 (0)