-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
chore(aci milestone 3): open period -> incident serializer #103674
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
c2b88d5
0ebff74
5ee3e76
dcf8b86
9f7b103
cfa5d0a
dcf50d8
c2d77f1
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 |
|---|---|---|
| @@ -1,95 +1,133 @@ | ||
| from collections import defaultdict | ||
| from collections.abc import Mapping, Sequence | ||
| from typing import Any, DefaultDict | ||
| from datetime import datetime | ||
| from typing import Any, ClassVar, DefaultDict | ||
|
|
||
| from django.contrib.auth.models import AnonymousUser | ||
| from django.db.models import Subquery | ||
|
|
||
| from sentry.api.serializers import Serializer, serialize | ||
| from sentry.incidents.endpoints.serializers.incident import ( | ||
| DetailedIncidentSerializerResponse, | ||
| IncidentSerializer, | ||
| IncidentSerializerResponse, | ||
| ) | ||
| from sentry.incidents.models.incident import Incident | ||
| from sentry.incidents.endpoints.serializers.utils import get_fake_id_from_object_id | ||
| from sentry.incidents.models.incident import IncidentStatus, IncidentStatusMethod, IncidentType | ||
| from sentry.models.groupopenperiod import GroupOpenPeriod | ||
| from sentry.models.groupopenperiodactivity import GroupOpenPeriodActivity | ||
| from sentry.snuba.entity_subscription import apply_dataset_query_conditions | ||
| from sentry.snuba.models import QuerySubscription, SnubaQuery | ||
| from sentry.types.group import PriorityLevel | ||
| from sentry.users.models.user import User | ||
| from sentry.users.services.user.model import RpcUser | ||
| from sentry.workflow_engine.endpoints.serializers.group_open_period_serializer import ( | ||
| GroupOpenPeriodActivitySerializer, | ||
| ) | ||
| from sentry.workflow_engine.models import ( | ||
| Action, | ||
| DataCondition, | ||
| DataConditionGroupAction, | ||
| AlertRuleDetector, | ||
| DataSourceDetector, | ||
| Detector, | ||
| DetectorWorkflow, | ||
| DetectorGroup, | ||
| IncidentGroupOpenPeriod, | ||
| WorkflowDataConditionGroup, | ||
| ) | ||
| from sentry.workflow_engine.models.workflow_action_group_status import WorkflowActionGroupStatus | ||
|
|
||
|
|
||
| class WorkflowEngineIncidentSerializer(Serializer): | ||
| def __init__(self, expand=None): | ||
| self.expand = expand or [] | ||
|
|
||
| priority_to_incident_status: ClassVar[dict[int, int]] = { | ||
| PriorityLevel.HIGH.value: IncidentStatus.CRITICAL.value, | ||
| PriorityLevel.MEDIUM.value: IncidentStatus.WARNING.value, | ||
| } | ||
|
|
||
| def get_incident_status(self, priority: int | None, date_ended: datetime | None) -> int: | ||
| if priority is None: | ||
| raise ValueError("Priority is required to get an incident status") | ||
|
|
||
| if date_ended: | ||
| return IncidentStatus.CLOSED.value | ||
|
|
||
| return self.priority_to_incident_status[priority] | ||
|
|
||
| def get_attrs( | ||
| self, | ||
| item_list: Sequence[GroupOpenPeriod], | ||
| user: User | RpcUser | AnonymousUser, | ||
| **kwargs: Any, | ||
| ) -> defaultdict[GroupOpenPeriod, dict[str, Any]]: | ||
|
|
||
| from sentry.incidents.endpoints.serializers.workflow_engine_detector import ( | ||
| WorkflowEngineDetectorSerializer, | ||
| ) | ||
|
|
||
| results: DefaultDict[GroupOpenPeriod, dict[str, Any]] = defaultdict() | ||
| open_periods_to_detectors = self.get_open_periods_to_detectors(item_list) | ||
| alert_rules = { | ||
| alert_rule["id"]: alert_rule # we are serializing detectors to look like alert rules | ||
| for alert_rule in serialize( | ||
| list(open_periods_to_detectors.values()), | ||
| user, | ||
| WorkflowEngineDetectorSerializer(expand=self.expand), | ||
| ) | ||
| } | ||
| alert_rule_detectors = AlertRuleDetector.objects.filter( | ||
| detector__in=list(open_periods_to_detectors.values()) | ||
| ).values_list("alert_rule_id", "detector_id") | ||
| detector_ids_to_alert_rule_ids = {} | ||
| for alert_rule_id, detector_id in alert_rule_detectors: | ||
| detector_ids_to_alert_rule_ids[detector_id] = alert_rule_id | ||
|
|
||
| for open_period in item_list: | ||
| detector_id = open_periods_to_detectors[open_period].id | ||
| if detector_id in detector_ids_to_alert_rule_ids: | ||
| alert_rule_id = detector_ids_to_alert_rule_ids[detector_id] | ||
| else: | ||
| alert_rule_id = get_fake_id_from_object_id(detector_id) | ||
|
|
||
| results[open_period] = {"projects": [open_period.project.slug]} | ||
| results[open_period]["alert_rule"] = alert_rules.get(str(alert_rule_id)) | ||
|
|
||
| if "activities" in self.expand: | ||
| gopas = list( | ||
| GroupOpenPeriodActivity.objects.filter(group_open_period__in=item_list)[:1000] | ||
| ) | ||
| open_period_activities = defaultdict(list) | ||
| # XXX: the incident endpoint is undocumented, so we aren' on the hook for supporting | ||
| # any specific payloads. Since this isn't used on the Sentry side for notification charts, | ||
| # I've opted to just use the GroupOpenPeriodActivity serializer. | ||
| for gopa, serialized_activity in zip( | ||
| gopas, | ||
| serialize(gopas, user=user, serializer=GroupOpenPeriodActivitySerializer()), | ||
| ): | ||
| open_period_activities[gopa.group_open_period_id].append(serialized_activity) | ||
| for open_period in item_list: | ||
| results[open_period]["activities"] = open_period_activities[open_period.id] | ||
|
|
||
| return results | ||
|
|
||
| def get_open_periods_to_detectors( | ||
| self, open_periods: Sequence[GroupOpenPeriod] | ||
| ) -> dict[GroupOpenPeriod, Detector]: | ||
| wf_action_group_statuses = WorkflowActionGroupStatus.objects.filter( | ||
| group__in=[open_period.group for open_period in open_periods] | ||
| ) | ||
| open_periods_to_actions: DefaultDict[GroupOpenPeriod, Action] = defaultdict() | ||
| for open_period in open_periods: | ||
| for wf_action_group_status in wf_action_group_statuses: | ||
| if wf_action_group_status.group == open_period.group: | ||
| open_periods_to_actions[open_period] = wf_action_group_status.action | ||
| break | ||
|
|
||
| dcgas = DataConditionGroupAction.objects.filter( | ||
| action__in=list(open_periods_to_actions.values()) | ||
| ) | ||
| open_periods_to_condition_group: DefaultDict[GroupOpenPeriod, DataConditionGroupAction] = ( | ||
| defaultdict() | ||
| ) | ||
| for open_period, action in open_periods_to_actions.items(): | ||
| for dcga in dcgas: | ||
| if dcga.action == action: | ||
| open_periods_to_condition_group[open_period] = dcga | ||
| break | ||
|
|
||
| action_filters = DataCondition.objects.filter( | ||
| condition_group__in=[dcga.condition_group for dcga in dcgas] | ||
| ) | ||
| open_period_to_action_filters: DefaultDict[GroupOpenPeriod, DataCondition] = defaultdict() | ||
| for open_period, dcga in open_periods_to_condition_group.items(): | ||
| for action_filter in action_filters: | ||
| if action_filter.condition_group == dcga.condition_group: | ||
| open_period_to_action_filters[open_period] = action_filter | ||
| break | ||
|
|
||
| workflow_dcgs = WorkflowDataConditionGroup.objects.filter( | ||
| condition_group__in=Subquery(action_filters.values("condition_group")) | ||
| ) | ||
| # open period -> group -> detector via detectorgroup | ||
| groups = [op.group for op in open_periods] | ||
| group_to_open_periods = defaultdict(list) | ||
|
|
||
| open_periods_to_workflow_dcgs: DefaultDict[GroupOpenPeriod, WorkflowDataConditionGroup] = ( | ||
| defaultdict() | ||
| ) | ||
| for open_period, action_filter in open_period_to_action_filters.items(): | ||
| for workflow_dcg in workflow_dcgs: | ||
| if workflow_dcg.condition_group == action_filter.condition_group: | ||
| open_periods_to_workflow_dcgs[open_period] = workflow_dcg | ||
| for op in open_periods: | ||
| group_to_open_periods[op.group].append(op) | ||
|
|
||
| detector_workflows = DetectorWorkflow.objects.filter( | ||
| workflow__in=Subquery(workflow_dcgs.values("workflow")) | ||
| detector_groups = DetectorGroup.objects.filter(group__in=groups).select_related( | ||
| "group", "detector" | ||
| ) | ||
| open_periods_to_detectors: DefaultDict[GroupOpenPeriod, Detector] = defaultdict() | ||
| for open_period, workflow_dcg in open_periods_to_workflow_dcgs.items(): | ||
| for detector_workflow in detector_workflows: | ||
| if detector_workflow.workflow == workflow_dcg.workflow: | ||
| open_periods_to_detectors[open_period] = detector_workflow.detector | ||
| break | ||
|
|
||
| groups_to_detectors = {} | ||
| for dg in detector_groups: | ||
| if dg.detector is not None: | ||
| groups_to_detectors[dg.group] = dg.detector | ||
|
|
||
| open_periods_to_detectors = {} | ||
| for group in group_to_open_periods: | ||
| for op in group_to_open_periods[group]: | ||
| open_periods_to_detectors[op] = groups_to_detectors[group] | ||
|
Comment on lines
+128
to
+130
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. Bug: 🔍 Detailed AnalysisThe 💡 Suggested FixEnsure 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews.
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. Bug: KeyError when DetectorGroup has null detectorThe code attempts to access |
||
|
|
||
| return open_periods_to_detectors | ||
|
|
||
|
|
@@ -103,10 +141,35 @@ def serialize( | |
| """ | ||
| Temporary serializer to take a GroupOpenPeriod and serialize it for the old incident endpoint | ||
| """ | ||
| incident = Incident.objects.get( | ||
| id=IncidentGroupOpenPeriod.objects.get(group_open_period=obj).incident_id | ||
| ) | ||
| return serialize(incident, user=user, serializer=IncidentSerializer(expand=self.expand)) | ||
| try: | ||
| igop = IncidentGroupOpenPeriod.objects.get(group_open_period=obj) | ||
| incident_id = igop.incident_id | ||
| incident_identifier = igop.incident_identifier | ||
| except IncidentGroupOpenPeriod.DoesNotExist: | ||
| incident_id = get_fake_id_from_object_id(obj.id) | ||
| incident_identifier = incident_id | ||
|
|
||
| date_closed = obj.date_ended.replace(second=0, microsecond=0) if obj.date_ended else None | ||
| return { | ||
| "id": str(incident_id), | ||
| "identifier": str(incident_identifier), | ||
| "organizationId": str(obj.project.organization.id), | ||
| "projects": attrs["projects"], | ||
| "alertRule": attrs["alert_rule"], | ||
| "activities": attrs["activities"] if "activities" in self.expand else None, | ||
| "status": self.get_incident_status(obj.group.priority, obj.date_ended), | ||
| "statusMethod": ( | ||
| IncidentStatusMethod.RULE_TRIGGERED.value | ||
| if not date_closed | ||
| else IncidentStatusMethod.RULE_UPDATED.value | ||
| ), | ||
| "type": IncidentType.ALERT_TRIGGERED.value, | ||
| "title": obj.group.title, | ||
| "dateStarted": obj.date_started, | ||
| "dateDetected": obj.date_started, | ||
| "dateCreated": obj.date_added, | ||
| "dateClosed": date_closed, | ||
| } | ||
|
|
||
|
|
||
| class WorkflowEngineDetailedIncidentSerializer(WorkflowEngineIncidentSerializer): | ||
|
|
||
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.
Bug: Missing LOW priority mapping causes KeyError
The
priority_to_incident_statusmapping only includes HIGH and MEDIUM priority levels, butPriorityLevel.LOW(value 25) is a valid priority. Whenget_incident_statusis called with a LOW priority group, line 50 raises aKeyErrorbecause LOW is missing from the mapping. This breaks serialization for any open period with a low-priority group.