5
5
from collections .abc import Sequence
6
6
from copy import deepcopy
7
7
from datetime import datetime , timedelta
8
- from typing import Literal , TypedDict , TypeVar , cast
8
+ from typing import TypeVar , cast
9
9
10
10
from django .conf import settings
11
11
from django .db import router , transaction
87
87
T = TypeVar ("T" )
88
88
89
89
90
- class MetricIssueDetectorConfig (TypedDict ):
91
- """
92
- Schema for Metric Issue Detector.config.
93
- """
94
-
95
- comparison_delta : int | None
96
- detection_type : Literal ["static" , "percent" , "dynamic" ]
97
-
98
-
99
90
class SubscriptionProcessor :
100
91
"""
101
92
Class for processing subscription updates for an alert rule. Accepts a subscription
@@ -116,20 +107,19 @@ class SubscriptionProcessor:
116
107
117
108
def __init__ (self , subscription : QuerySubscription ) -> None :
118
109
self .subscription = subscription
119
- self ._alert_rule : AlertRule | None = None
120
110
try :
121
- self ._alert_rule = AlertRule .objects .get_for_subscription (subscription )
111
+ self .alert_rule = AlertRule .objects .get_for_subscription (subscription )
122
112
except AlertRule .DoesNotExist :
123
113
return
124
114
125
- self .triggers = AlertRuleTrigger .objects .get_for_alert_rule (self ._alert_rule )
115
+ self .triggers = AlertRuleTrigger .objects .get_for_alert_rule (self .alert_rule )
126
116
self .triggers .sort (key = lambda trigger : trigger .alert_threshold )
127
117
128
118
(
129
119
self .last_update ,
130
120
self .trigger_alert_counts ,
131
121
self .trigger_resolve_counts ,
132
- ) = get_alert_rule_stats (self ._alert_rule , self .subscription , self .triggers )
122
+ ) = get_alert_rule_stats (self .alert_rule , self .subscription , self .triggers )
133
123
self .orig_trigger_alert_counts = deepcopy (self .trigger_alert_counts )
134
124
self .orig_trigger_resolve_counts = deepcopy (self .trigger_resolve_counts )
135
125
@@ -145,14 +135,6 @@ def __init__(self, subscription: QuerySubscription) -> None:
145
135
or self ._has_workflow_engine_processing_only
146
136
)
147
137
148
- @property
149
- def alert_rule (self ) -> AlertRule :
150
- """
151
- Only use this in non-single processing contexts.
152
- """
153
- assert self ._alert_rule is not None
154
- return self ._alert_rule
155
-
156
138
@property
157
139
def active_incident (self ) -> Incident | None :
158
140
"""
@@ -206,15 +188,15 @@ def check_trigger_matches_status(
206
188
incident_trigger = self .incident_trigger_map .get (trigger .id )
207
189
return incident_trigger is not None and incident_trigger .status == status .value
208
190
209
- def reset_trigger_counts (self , alert_rule : AlertRule ) -> None :
191
+ def reset_trigger_counts (self ) -> None :
210
192
"""
211
193
Helper method that clears both the trigger alert and the trigger resolve counts
212
194
"""
213
195
for trigger_id in self .trigger_alert_counts :
214
196
self .trigger_alert_counts [trigger_id ] = 0
215
197
for trigger_id in self .trigger_resolve_counts :
216
198
self .trigger_resolve_counts [trigger_id ] = 0
217
- self .update_alert_rule_stats (alert_rule )
199
+ self .update_alert_rule_stats ()
218
200
219
201
def calculate_resolve_threshold (self , trigger : AlertRuleTrigger ) -> float :
220
202
"""
@@ -271,8 +253,8 @@ def get_crash_rate_alert_metrics_aggregation_value(
271
253
aggregation_value = get_crash_rate_alert_metrics_aggregation_value_helper (
272
254
subscription_update
273
255
)
274
- if aggregation_value is None and self . _alert_rule is not None :
275
- self .reset_trigger_counts (self . _alert_rule )
256
+ if aggregation_value is None :
257
+ self .reset_trigger_counts ()
276
258
return aggregation_value
277
259
278
260
def get_aggregation_value (
@@ -289,7 +271,7 @@ def get_aggregation_value(
289
271
organization_id = self .subscription .project .organization .id ,
290
272
project_ids = [self .subscription .project_id ],
291
273
comparison_delta = comparison_delta ,
292
- alert_rule_id = self ._alert_rule .id if self . _alert_rule else None ,
274
+ alert_rule_id = self .alert_rule .id ,
293
275
)
294
276
295
277
return aggregation_value
@@ -318,7 +300,7 @@ def handle_trigger_anomalies(
318
300
is_resolved = False ,
319
301
)
320
302
incremented = metrics_incremented or incremented
321
- incident_trigger = self .trigger_alert_threshold (trigger )
303
+ incident_trigger = self .trigger_alert_threshold (trigger , aggregation_value )
322
304
if incident_trigger is not None :
323
305
fired_incident_triggers .append (incident_trigger )
324
306
else :
@@ -350,12 +332,9 @@ def get_comparison_delta(self, detector: Detector | None) -> int | None:
350
332
comparison_delta = None
351
333
352
334
if detector :
353
- detector_cfg : MetricIssueDetectorConfig = detector .config
354
- comparison_delta = detector_cfg .get ("comparison_delta" )
335
+ comparison_delta = detector .config .get ("comparison_delta" )
355
336
else :
356
- # If we don't have a Detector, we must have an AlertRule.
357
- assert self ._alert_rule is not None
358
- comparison_delta = self ._alert_rule .comparison_delta
337
+ comparison_delta = self .alert_rule .comparison_delta
359
338
360
339
return comparison_delta
361
340
@@ -442,7 +421,7 @@ def handle_trigger_alerts(
442
421
)
443
422
incremented = metrics_incremented or incremented
444
423
# triggering a threshold will create an incident and set the status to active
445
- incident_trigger = self .trigger_alert_threshold (trigger )
424
+ incident_trigger = self .trigger_alert_threshold (trigger , aggregation_value )
446
425
if incident_trigger is not None :
447
426
fired_incident_triggers .append (incident_trigger )
448
427
else :
@@ -476,13 +455,11 @@ def handle_trigger_alerts(
476
455
477
456
def process_results_workflow_engine (
478
457
self ,
479
- detector : Detector ,
480
458
subscription_update : QuerySubscriptionUpdate ,
481
459
aggregation_value : float ,
482
460
organization : Organization ,
483
461
) -> list [tuple [Detector , dict [DetectorGroupKey , DetectorEvaluationResult ]]]:
484
- detector_cfg : MetricIssueDetectorConfig = detector .config
485
- if detector_cfg ["detection_type" ] == AlertRuleDetectionType .DYNAMIC .value :
462
+ if self .alert_rule .detection_type == AlertRuleDetectionType .DYNAMIC :
486
463
anomaly_detection_packet = AnomalyDetectionUpdate (
487
464
entity = subscription_update .get ("entity" , "" ),
488
465
subscription_id = subscription_update ["subscription_id" ],
@@ -522,13 +499,14 @@ def process_results_workflow_engine(
522
499
"results" : results ,
523
500
"num_results" : len (results ),
524
501
"value" : aggregation_value ,
525
- "rule_id" : self ._alert_rule .id if self . _alert_rule else None ,
502
+ "rule_id" : self .alert_rule .id ,
526
503
},
527
504
)
528
505
return results
529
506
530
507
def process_legacy_metric_alerts (
531
508
self ,
509
+ subscription_update : QuerySubscriptionUpdate ,
532
510
aggregation_value : float ,
533
511
detector : Detector | None ,
534
512
results : list [tuple [Detector , dict [DetectorGroupKey , DetectorEvaluationResult ]]] | None ,
@@ -654,7 +632,7 @@ def process_legacy_metric_alerts(
654
632
# is killed here. The trade-off is that we might process an update twice. Mostly
655
633
# this will have no effect, but if someone manages to close a triggered incident
656
634
# before the next one then we might alert twice.
657
- self .update_alert_rule_stats (self . alert_rule )
635
+ self .update_alert_rule_stats ()
658
636
return fired_incident_triggers
659
637
660
638
def has_downgraded (self , dataset : str , organization : Organization ) -> bool :
@@ -699,7 +677,7 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> None:
699
677
if self .has_downgraded (dataset , organization ):
700
678
return
701
679
702
- if self . _alert_rule is None :
680
+ if not hasattr ( self , "alert_rule" ) :
703
681
# QuerySubscriptions must _always_ have an associated AlertRule
704
682
# If the alert rule has been removed then clean up associated tables and return
705
683
metrics .incr ("incidents.alert_rules.no_alert_rule_for_subscription" , sample_rate = 1.0 )
@@ -758,9 +736,8 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> None:
758
736
legacy_results = None
759
737
760
738
if self ._has_workflow_engine_processing :
761
- assert detector is not None
762
739
workflow_engine_results = self .process_results_workflow_engine (
763
- detector , subscription_update , aggregation_value , organization
740
+ subscription_update , aggregation_value , organization
764
741
)
765
742
766
743
if self ._has_workflow_engine_processing_only :
@@ -779,6 +756,7 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> None:
779
756
workflow engine "and" metric alerts.
780
757
"""
781
758
legacy_results = self .process_legacy_metric_alerts (
759
+ subscription_update ,
782
760
aggregation_value ,
783
761
detector ,
784
762
workflow_engine_results ,
@@ -797,8 +775,7 @@ def process_update(self, subscription_update: QuerySubscriptionUpdate) -> None:
797
775
)
798
776
799
777
def trigger_alert_threshold (
800
- self ,
801
- trigger : AlertRuleTrigger ,
778
+ self , trigger : AlertRuleTrigger , metric_value : float
802
779
) -> IncidentTrigger | None :
803
780
"""
804
781
Called when a subscription update exceeds the value defined in the
@@ -1042,7 +1019,7 @@ def handle_incident_severity_update(self) -> None:
1042
1019
status_method = IncidentStatusMethod .RULE_TRIGGERED ,
1043
1020
)
1044
1021
1045
- def update_alert_rule_stats (self , alert_rule : AlertRule ) -> None :
1022
+ def update_alert_rule_stats (self ) -> None :
1046
1023
"""
1047
1024
Updates stats about the alert rule, if they're changed.
1048
1025
:return:
@@ -1059,7 +1036,7 @@ def update_alert_rule_stats(self, alert_rule: AlertRule) -> None:
1059
1036
}
1060
1037
1061
1038
update_alert_rule_stats (
1062
- alert_rule ,
1039
+ self . alert_rule ,
1063
1040
self .subscription ,
1064
1041
self .last_update ,
1065
1042
updated_trigger_alert_counts ,
0 commit comments