2
2
3
3
from dataclasses import dataclass
4
4
from datetime import UTC , datetime
5
- from typing import Any
6
5
7
6
from sentry import features
7
+ from sentry .constants import CRASH_RATE_ALERT_AGGREGATE_ALIAS
8
8
from sentry .incidents .handlers .condition import * # noqa
9
9
from sentry .incidents .metric_alert_detector import MetricAlertsDetectorValidator
10
10
from sentry .incidents .models .alert_rule import AlertRuleDetectionType , ComparisonDeltaChoices
11
+ from sentry .incidents .utils .format_duration import format_duration_idiomatic
12
+ from sentry .incidents .utils .metric_issue_poc import QUERY_AGGREGATION_DISPLAY
11
13
from sentry .incidents .utils .types import QuerySubscriptionUpdate
14
+ from sentry .integrations .metric_alerts import TEXT_COMPARISON_DELTA
12
15
from sentry .issues .grouptype import GroupCategory , GroupType
13
16
from sentry .models .organization import Organization
14
17
from sentry .ratelimits .sliding_windows import Quota
18
+ from sentry .snuba .metrics import format_mri_field , is_mri_field
19
+ from sentry .snuba .models import QuerySubscription , SnubaQuery
20
+ from sentry .types .actor import parse_and_validate_actor
15
21
from sentry .types .group import PriorityLevel
16
22
from sentry .workflow_engine .handlers .detector import DetectorOccurrence , StatefulDetectorHandler
17
- from sentry .workflow_engine .handlers .detector .base import EvidenceData
23
+ from sentry .workflow_engine .handlers .detector .base import EventData , EvidenceData
24
+ from sentry .workflow_engine .models .alertrule_detector import AlertRuleDetector
25
+ from sentry .workflow_engine .models .data_condition import Condition , DataCondition
18
26
from sentry .workflow_engine .models .data_source import DataPacket
19
27
from sentry .workflow_engine .processors .data_condition_group import ProcessedDataConditionGroup
20
- from sentry .workflow_engine .types import DetectorPriorityLevel , DetectorSettings
28
+ from sentry .workflow_engine .types import DetectorException , DetectorPriorityLevel , DetectorSettings
21
29
22
30
COMPARISON_DELTA_CHOICES : list [None | int ] = [choice .value for choice in ComparisonDeltaChoices ]
23
31
COMPARISON_DELTA_CHOICES .append (None )
@@ -28,29 +36,130 @@ class MetricIssueEvidenceData(EvidenceData):
28
36
alert_id : int
29
37
30
38
31
- class MetricAlertDetectorHandler (StatefulDetectorHandler [QuerySubscriptionUpdate , int ]):
39
+ class MetricIssueDetectorHandler (StatefulDetectorHandler [QuerySubscriptionUpdate , int ]):
32
40
def create_occurrence (
33
41
self ,
34
42
evaluation_result : ProcessedDataConditionGroup ,
35
43
data_packet : DataPacket [QuerySubscriptionUpdate ],
36
44
priority : DetectorPriorityLevel ,
37
- ) -> tuple [DetectorOccurrence , dict [str , Any ]]:
38
- # Returning a placeholder for now, this may require us passing more info
39
- occurrence = DetectorOccurrence (
40
- issue_title = "Some Issue Title" ,
41
- subtitle = "An Issue Subtitle" ,
42
- type = MetricIssue ,
43
- level = "error" ,
44
- culprit = "Some culprit" ,
45
+ ) -> tuple [DetectorOccurrence , EventData ]:
46
+ try :
47
+ alert_rule_detector = AlertRuleDetector .objects .get (detector = self .detector )
48
+ alert_id = alert_rule_detector .alert_rule_id
49
+ except AlertRuleDetector .DoesNotExist :
50
+ alert_id = None
51
+
52
+ try :
53
+ detector_trigger = DataCondition .objects .get (
54
+ condition_group = self .detector .workflow_condition_group , condition_result = priority
55
+ )
56
+ except DataCondition .DoesNotExist :
57
+ raise DetectorException (
58
+ f"Failed to find detector trigger for detector id { self .detector .id } , cannot create metric issue occurrence"
59
+ )
60
+
61
+ try :
62
+ query_subscription = QuerySubscription .objects .get (id = data_packet .source_id )
63
+ except QuerySubscription .DoesNotExist :
64
+ raise DetectorException (
65
+ f"Failed to find query subscription for detector id { self .detector .id } , cannot create metric issue occurrence"
66
+ )
67
+
68
+ try :
69
+ snuba_query = SnubaQuery .objects .get (id = query_subscription .snuba_query_id )
70
+ except SnubaQuery .DoesNotExist :
71
+ raise DetectorException (
72
+ f"Failed to find snuba query for detector id { self .detector .id } , cannot create metric issue occurrence"
73
+ )
74
+
75
+ try :
76
+ assignee = parse_and_validate_actor (
77
+ str (self .detector .created_by_id ), self .detector .project .organization_id
78
+ )
79
+ except Exception :
80
+ assignee = None
81
+
82
+ title = self .construct_title (snuba_query , detector_trigger , priority )
83
+ event_data = {
84
+ "environment" : self .detector .config .get ("environment" ),
85
+ "platform" : None ,
86
+ "sdk" : None ,
87
+ } # XXX: may need to add to this
88
+
89
+ return (
90
+ DetectorOccurrence (
91
+ issue_title = self .detector .name ,
92
+ subtitle = title ,
93
+ resource_id = None ,
94
+ evidence_data = {
95
+ "alert_id" : alert_id ,
96
+ },
97
+ evidence_display = [], # XXX: may need to pass more info here for the front end
98
+ type = MetricIssue ,
99
+ level = "error" ,
100
+ culprit = "" ,
101
+ priority = priority ,
102
+ assignee = assignee ,
103
+ ),
104
+ event_data ,
45
105
)
46
- return occurrence , {}
47
106
48
107
def extract_dedupe_value (self , data_packet : DataPacket [QuerySubscriptionUpdate ]) -> int :
49
108
return int (data_packet .packet .get ("timestamp" , datetime .now (UTC )).timestamp ())
50
109
51
110
def extract_value (self , data_packet : DataPacket [QuerySubscriptionUpdate ]) -> int :
52
111
return data_packet .packet ["values" ]["value" ]
53
112
113
+ def construct_title (
114
+ self ,
115
+ snuba_query : SnubaQuery ,
116
+ detector_trigger : DataCondition ,
117
+ priority : DetectorPriorityLevel ,
118
+ ) -> str :
119
+ comparison_delta = self .detector .config .get ("comparison_delta" )
120
+ agg_display_key = snuba_query .aggregate
121
+
122
+ if is_mri_field (agg_display_key ):
123
+ aggregate = format_mri_field (agg_display_key )
124
+ elif CRASH_RATE_ALERT_AGGREGATE_ALIAS in agg_display_key :
125
+ agg_display_key = agg_display_key .split (f"AS { CRASH_RATE_ALERT_AGGREGATE_ALIAS } " )[
126
+ 0
127
+ ].strip ()
128
+ aggregate = QUERY_AGGREGATION_DISPLAY .get (agg_display_key , agg_display_key )
129
+ else :
130
+ aggregate = QUERY_AGGREGATION_DISPLAY .get (agg_display_key , agg_display_key )
131
+
132
+ # Determine the higher or lower comparison
133
+ higher_or_lower = ""
134
+ if detector_trigger .type == Condition .GREATER :
135
+ higher_or_lower = "greater than" if comparison_delta else "above"
136
+ else :
137
+ higher_or_lower = "less than" if comparison_delta else "below"
138
+
139
+ label = "Warning" if priority == DetectorPriorityLevel .MEDIUM else "Critical"
140
+
141
+ # Format the time window for the threshold
142
+ time_window = format_duration_idiomatic (snuba_query .time_window // 60 )
143
+
144
+ # If the detector_trigger has a comparison delta, format the comparison string
145
+ comparison : str | int | float = "threshold"
146
+ if comparison_delta :
147
+ comparison_delta_minutes = comparison_delta // 60
148
+ comparison = TEXT_COMPARISON_DELTA .get (
149
+ comparison_delta_minutes , f"same time { comparison_delta_minutes } minutes ago "
150
+ )
151
+ else :
152
+ comparison = detector_trigger .comparison
153
+
154
+ template = "{label}: {metric} in the last {time_window} {higher_or_lower} {comparison}"
155
+ return template .format (
156
+ label = label .capitalize (),
157
+ metric = aggregate ,
158
+ higher_or_lower = higher_or_lower ,
159
+ comparison = comparison ,
160
+ time_window = time_window ,
161
+ )
162
+
54
163
55
164
# Example GroupType and detector handler for metric alerts. We don't create these issues yet, but we'll use something
56
165
# like these when we're sending issues as alerts
@@ -65,8 +174,9 @@ class MetricIssue(GroupType):
65
174
default_priority = PriorityLevel .HIGH
66
175
enable_auto_resolve = False
67
176
enable_escalation_detection = False
177
+ enable_status_change_workflow_notifications = False
68
178
detector_settings = DetectorSettings (
69
- handler = MetricAlertDetectorHandler ,
179
+ handler = MetricIssueDetectorHandler ,
70
180
validator = MetricAlertsDetectorValidator ,
71
181
config_schema = {
72
182
"$schema" : "https://json-schema.org/draft/2020-12/schema" ,
0 commit comments