Skip to content

Commit 69393bd

Browse files
authored
Merge pull request #1768 from elementary-data/ele-3893-alert-rules-operators
Ele 3893 alert rules operators
2 parents d2d21e3 + cb191c1 commit 69393bd

File tree

6 files changed

+672
-441
lines changed

6 files changed

+672
-441
lines changed

.vscode/launch.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@
88
"module": "elementary.cli.cli",
99
"console": "integratedTerminal",
1010
"args": "${command:pickArgs}"
11+
},
12+
{
13+
"name": "pytest: Current File",
14+
"type": "debugpy",
15+
"request": "launch",
16+
"module": "pytest",
17+
"args": ["-vvv", "-s", "${file}"],
18+
"console": "integratedTerminal"
19+
},
20+
{
21+
"name": "pytest: Selector",
22+
"type": "debugpy",
23+
"request": "launch",
24+
"module": "pytest",
25+
"args": ["-vvv", "-s", "${file}::${input:selector}"],
26+
"console": "integratedTerminal"
27+
}
28+
],
29+
"inputs": [
30+
{
31+
"id": "selector",
32+
"type": "promptString",
33+
"description": "Selector"
1134
}
1235
]
1336
}
Lines changed: 61 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
from functools import reduce
2-
from typing import List
1+
from typing import List, Optional
32

43
from elementary.monitor.data_monitoring.schema import (
5-
FilterSchema,
64
FiltersSchema,
7-
ResourceTypeFilterSchema,
8-
StatusFilterSchema,
5+
ResourceType,
6+
Status,
97
)
108
from elementary.monitor.fetchers.alerts.schema.pending_alerts import (
119
AlertTypes,
@@ -16,6 +14,61 @@
1614
logger = get_logger(__name__)
1715

1816

17+
def get_string_ends(input_string: str, splitter: str) -> List[str]:
18+
parts = input_string.split(splitter)
19+
result = []
20+
21+
for i in range(len(parts)):
22+
result.append(splitter.join(parts[i:]))
23+
24+
return result
25+
26+
27+
def _get_alert_node_name(alert: PendingAlertSchema) -> Optional[str]:
28+
alert_node_name = None
29+
alert_type = AlertTypes(alert.type)
30+
if alert_type is AlertTypes.TEST:
31+
alert_node_name = alert.data.test_name # type: ignore[union-attr]
32+
elif alert_type is AlertTypes.MODEL or alert_type is AlertTypes.SOURCE_FRESHNESS:
33+
alert_node_name = alert.data.model_unique_id
34+
else:
35+
raise ValueError(f"Unexpected alert type: {alert_type}")
36+
return alert_node_name
37+
38+
39+
def apply_filters_schema_on_alert(
40+
alert: PendingAlertSchema, filters_schema: FiltersSchema
41+
) -> bool:
42+
tags = alert.data.tags or []
43+
models = (
44+
[
45+
alert.data.model_unique_id,
46+
*get_string_ends(alert.data.model_unique_id, "."),
47+
]
48+
if alert.data.model_unique_id
49+
else []
50+
)
51+
owners = alert.data.unified_owners or []
52+
status = Status(alert.data.status)
53+
resource_type = ResourceType(alert.data.resource_type)
54+
55+
alert_node_name = _get_alert_node_name(alert)
56+
node_names = (
57+
[alert_node_name, *get_string_ends(alert_node_name, ".")]
58+
if alert_node_name
59+
else []
60+
)
61+
62+
return filters_schema.apply(
63+
tags=tags,
64+
models=models,
65+
owners=owners,
66+
statuses=[status],
67+
resource_types=[resource_type],
68+
node_names=node_names,
69+
)
70+
71+
1972
def filter_alerts(
2073
alerts: List[PendingAlertSchema],
2174
alerts_filter: FiltersSchema = FiltersSchema(),
@@ -29,188 +82,6 @@ def filter_alerts(
2982
logger.warning("Invalid filter for alerts: %s", alerts_filter.selector)
3083
return [] # type: ignore[return-value]
3184

32-
# If the filter is empty, we want to return all of the alerts
33-
filtered_alerts = alerts
34-
filtered_alerts = _filter_alerts_by_tags(filtered_alerts, alerts_filter.tags)
35-
filtered_alerts = _filter_alerts_by_models(filtered_alerts, alerts_filter.models)
36-
filtered_alerts = _filter_alerts_by_owners(filtered_alerts, alerts_filter.owners)
37-
filtered_alerts = _filter_alerts_by_statuses(
38-
filtered_alerts, alerts_filter.statuses
39-
)
40-
filtered_alerts = _filter_alerts_by_resource_types(
41-
filtered_alerts, alerts_filter.resource_types
42-
)
43-
if alerts_filter.node_names:
44-
filtered_alerts = _filter_alerts_by_node_names(
45-
filtered_alerts, alerts_filter.node_names
46-
)
47-
48-
return filtered_alerts
49-
50-
51-
def _find_common_alerts(
52-
first_alerts: List[PendingAlertSchema],
53-
second_alerts: List[PendingAlertSchema],
54-
) -> List[PendingAlertSchema]:
55-
first_alert_ids = [alert.id for alert in first_alerts]
56-
second_alert_ids = [alert.id for alert in second_alerts]
57-
common_alert_ids = list(set(first_alert_ids) & set(second_alert_ids))
58-
59-
common_alerts = []
60-
# To handle dedupping common alerts
61-
alert_ids_already_handled = []
62-
63-
for alert in [*first_alerts, *second_alerts]:
64-
if alert.id in common_alert_ids and alert.id not in alert_ids_already_handled:
65-
common_alerts.append(alert)
66-
alert_ids_already_handled.append(alert.id)
67-
return common_alerts
68-
69-
70-
def _filter_alerts_by_tags(
71-
alerts: List[PendingAlertSchema],
72-
tags_filters: List[FilterSchema],
73-
) -> List[PendingAlertSchema]:
74-
if not tags_filters:
75-
return [*alerts]
76-
77-
grouped_filtered_alerts_by_tags = []
78-
79-
# OR filter for each tags_filter's values
80-
for tags_filter in tags_filters:
81-
filtered_alerts_by_tags = []
82-
for alert in alerts:
83-
if any(tag in (alert.data.tags or []) for tag in tags_filter.values):
84-
filtered_alerts_by_tags.append(alert)
85-
grouped_filtered_alerts_by_tags.append(filtered_alerts_by_tags)
86-
87-
# AND filter between all tags_filters
88-
return reduce(_find_common_alerts, grouped_filtered_alerts_by_tags)
89-
90-
91-
def _filter_alerts_by_owners(
92-
alerts: List[PendingAlertSchema],
93-
owners_filters: List[FilterSchema],
94-
) -> List[PendingAlertSchema]:
95-
if not owners_filters:
96-
return [*alerts]
97-
98-
grouped_filtered_alerts_by_owners = []
99-
100-
# OR filter for each owners_filter's values
101-
for owners_filter in owners_filters:
102-
filtered_alerts_by_owners = []
103-
for alert in alerts:
104-
if any(
105-
owner in alert.data.unified_owners for owner in owners_filter.values
106-
):
107-
filtered_alerts_by_owners.append(alert)
108-
grouped_filtered_alerts_by_owners.append(filtered_alerts_by_owners)
109-
110-
# AND filter between all owners_filters
111-
return reduce(_find_common_alerts, grouped_filtered_alerts_by_owners)
112-
113-
114-
def _filter_alerts_by_models(
115-
alerts: List[PendingAlertSchema],
116-
models_filters: List[FilterSchema],
117-
) -> List[PendingAlertSchema]:
118-
if not models_filters:
119-
return [*alerts]
120-
121-
grouped_filtered_alerts_by_models = []
122-
123-
# OR filter for each models_filter's values
124-
for models_filter in models_filters:
125-
filtered_alerts_by_models = []
126-
for alert in alerts:
127-
if any(
128-
(
129-
alert.data.model_unique_id
130-
and alert.data.model_unique_id.endswith(model)
131-
)
132-
for model in models_filter.values
133-
):
134-
filtered_alerts_by_models.append(alert)
135-
grouped_filtered_alerts_by_models.append(filtered_alerts_by_models)
136-
137-
# AND filter between all models_filters
138-
return reduce(_find_common_alerts, grouped_filtered_alerts_by_models)
139-
140-
141-
def _filter_alerts_by_node_names(
142-
alerts: List[PendingAlertSchema],
143-
node_names_filters: List[str],
144-
) -> List[PendingAlertSchema]:
145-
if not node_names_filters:
146-
return [*alerts]
147-
148-
filtered_alerts = []
149-
for alert in alerts:
150-
alert_node_name = None
151-
alert_type = AlertTypes(alert.type)
152-
if alert_type is AlertTypes.TEST:
153-
alert_node_name = alert.data.test_name # type: ignore[union-attr]
154-
elif (
155-
alert_type is AlertTypes.MODEL or alert_type is AlertTypes.SOURCE_FRESHNESS
156-
):
157-
alert_node_name = alert.data.model_unique_id
158-
else:
159-
# Shouldn't happen
160-
raise Exception(f"Unexpected alert type: {type(alert)}")
161-
162-
if alert_node_name:
163-
for node_name in node_names_filters:
164-
if alert_node_name.endswith(node_name) or node_name.endswith(
165-
alert_node_name
166-
):
167-
filtered_alerts.append(alert)
168-
break
169-
return filtered_alerts # type: ignore[return-value]
170-
171-
172-
def _filter_alerts_by_statuses(
173-
alerts: List[PendingAlertSchema],
174-
statuses_filters: List[StatusFilterSchema],
175-
) -> List[PendingAlertSchema]:
176-
if not statuses_filters:
177-
return [*alerts]
178-
179-
grouped_filtered_alerts_by_statuses = []
180-
181-
# OR filter for each statuses_filter's values
182-
for statuses_filter in statuses_filters:
183-
filtered_alerts_by_statuses = []
184-
for alert in alerts:
185-
if any(status == alert.data.status for status in statuses_filter.values):
186-
filtered_alerts_by_statuses.append(alert)
187-
grouped_filtered_alerts_by_statuses.append(filtered_alerts_by_statuses)
188-
189-
# AND filter between all statuses_filters
190-
return reduce(_find_common_alerts, grouped_filtered_alerts_by_statuses)
191-
192-
193-
def _filter_alerts_by_resource_types(
194-
alerts: List[PendingAlertSchema],
195-
resource_types_filters: List[ResourceTypeFilterSchema],
196-
) -> List[PendingAlertSchema]:
197-
if not resource_types_filters:
198-
return [*alerts]
199-
200-
grouped_filtered_alerts_by_resource_types = []
201-
202-
# OR filter for each resource_types_filter's values
203-
for resource_types_filter in resource_types_filters:
204-
filtered_alerts_by_resource_types = []
205-
for alert in alerts:
206-
if any(
207-
resource_type == alert.data.resource_type.value
208-
for resource_type in resource_types_filter.values
209-
):
210-
filtered_alerts_by_resource_types.append(alert)
211-
grouped_filtered_alerts_by_resource_types.append(
212-
filtered_alerts_by_resource_types
213-
)
214-
215-
# AND filter between all resource_types_filters
216-
return reduce(_find_common_alerts, grouped_filtered_alerts_by_resource_types)
85+
return [
86+
alert for alert in alerts if apply_filters_schema_on_alert(alert, alerts_filter)
87+
]

0 commit comments

Comments
 (0)