Skip to content

Commit acd0c44

Browse files
Konstantinov-InnokentiiMaxim MordasovFerril
authored
Support prescribed labels (#3848)
# What this PR does **Cleanup label typing:** 1. LabelParam -> two separate types LabekKey and LabelValue 2. LabelData -> renamed to LabelPair. 3. LabelKeyData -> renamed to LabelOption Data is not giving any info about what this type represents. 4. Remove LabelsData and LabelsKeysData types. They are just list of types listed above and with new naming it feels obsolete. 5. ValueData removed. LabelPair is used instead. 6. Rework AlertGroupCustomLabel to use LabelKey type for key to make type system more consistent. Name model type AlertGroupCustomLabel**DB** and api type AlertGroupCustomLabel**API** to clearly distinguish them. **Split update_labels_cache into two tasks** update_label_option_cache and update_label_pairs_cache. Original task was expecting array of LabelsData (now it's LabelPair) OR one LabelKeyData ( now it's LabelOption). I believe having one function with two sp different argument types makes it more complicated for understanding. **Make OnCall backend support prescribed labels**. OnCall will sync and store "prescribed" field for key and values, so Label dropdown able to disable editing for certain labels. ## Which issue(s) this PR fixes ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Maxim Mordasov <[email protected]> Co-authored-by: Yulya Artyukhina <[email protected]>
1 parent fde2214 commit acd0c44

29 files changed

+635
-269
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
– Support prescribed labels ([#3848](https://github.com/grafana/oncall/pull/3848))
1213
- Add status change trigger type to webhooks ([#3920](https://github.com/grafana/oncall/pull/3920))
1314

1415
### Fixed

engine/apps/alerts/models/alert.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from apps.alerts.signals import alert_group_escalation_snapshot_built
1616
from apps.alerts.tasks.distribute_alert import send_alert_create_signal
1717
from apps.labels.alert_group_labels import assign_labels, gather_labels_from_alert_receive_channel_and_raw_request_data
18-
from apps.labels.types import Labels
18+
from apps.labels.types import AlertLabels
1919
from common.jinja_templater import apply_jinja_template_to_alert_payload_and_labels
2020
from common.jinja_templater.apply_jinja_template import (
2121
JinjaTemplateError,
@@ -221,7 +221,7 @@ def _apply_jinja_template_to_alert_payload_and_labels(
221221
template_name: str,
222222
alert_receive_channel: "AlertReceiveChannel",
223223
raw_request_data: RawRequestData,
224-
labels: typing.Optional[Labels],
224+
labels: typing.Optional[AlertLabels],
225225
use_error_msg_as_fallback=False,
226226
check_if_templated_value_is_truthy=False,
227227
) -> typing.Union[str, None, bool]:
@@ -246,7 +246,7 @@ def render_group_data(
246246
cls,
247247
alert_receive_channel: "AlertReceiveChannel",
248248
raw_request_data: RawRequestData,
249-
labels: typing.Optional[Labels],
249+
labels: typing.Optional[AlertLabels],
250250
is_demo=False,
251251
) -> "AlertGroup.GroupData":
252252
from apps.alerts.models import AlertGroup

engine/apps/alerts/models/alert_receive_channel.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
294294
rate_limited_in_slack_at = models.DateTimeField(null=True, default=None)
295295
rate_limit_message_task_id = models.CharField(max_length=100, null=True, default=None)
296296

297-
AlertGroupCustomLabels = list[tuple[str, str | None, str | None]] | None
298-
alert_group_labels_custom: AlertGroupCustomLabels = models.JSONField(null=True, default=None)
297+
AlertGroupCustomLabelsDB = list[tuple[str, str | None, str | None]] | None
298+
alert_group_labels_custom: AlertGroupCustomLabelsDB = models.JSONField(null=True, default=None)
299299
"""
300300
Stores "custom labels" for alert group labels. Custom labels can be either "plain" or "templated".
301301
For plain labels, the format is: [<LABEL_KEY_ID>, <LABEL_VALUE_ID>, None]

engine/apps/alerts/models/channel_filter.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from django.db.models.manager import RelatedManager
2121

2222
from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel
23-
from apps.labels.types import Labels
23+
from apps.labels.types import AlertLabels
2424

2525
logger = logging.getLogger(__name__)
2626

@@ -120,12 +120,12 @@ def select_filter(
120120
return None
121121

122122
def is_satisfying(
123-
self, raw_request_data: "Alert.RawRequestData", alert_labels: typing.Optional["Labels"] = None
123+
self, raw_request_data: "Alert.RawRequestData", alert_labels: typing.Optional["AlertLabels"] = None
124124
) -> bool:
125125
return self.is_default or self.check_filter(raw_request_data, alert_labels)
126126

127127
def check_filter(
128-
self, raw_request_data: "Alert.RawRequestData", alert_labels: typing.Optional["Labels"] = None
128+
self, raw_request_data: "Alert.RawRequestData", alert_labels: typing.Optional["AlertLabels"] = None
129129
) -> bool:
130130
if self.filtering_term_type == ChannelFilter.FILTERING_TERM_TYPE_JINJA2:
131131
try:

engine/apps/api/serializers/alert_receive_channel.py

+49-30
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from apps.base.messaging import get_messaging_backends
1616
from apps.integrations.legacy_prefix import has_legacy_prefix
1717
from apps.labels.models import LabelKeyCache, LabelValueCache
18+
from apps.labels.types import LabelKey
1819
from apps.user_management.models import Organization
1920
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
2021
from common.api_helpers.exceptions import BadRequest
@@ -25,41 +26,45 @@
2526
from .labels import LabelsSerializerMixin
2627

2728

28-
class AlertGroupCustomLabelKey(typing.TypedDict):
29-
id: str
30-
name: str
31-
32-
33-
class AlertGroupCustomLabelValue(typing.TypedDict):
29+
# AlertGroupCustomLabelValue represents custom alert group label value for API requests
30+
# It handles two types of label's value:
31+
# 1. Just Label Value from a label repo for a static label
32+
# 2. Templated Label value which is actually a jinja template for a dynamic label.
33+
class AlertGroupCustomLabelValueAPI(typing.TypedDict):
3434
id: str | None # None for templated labels, label value ID for plain labels
3535
name: str # Jinja template for templated labels, label value name for plain labels
36+
prescribed: bool # Indicates of selected label value is prescribed. Not applicable for templated values.
3637

3738

38-
class AlertGroupCustomLabel(typing.TypedDict):
39-
key: AlertGroupCustomLabelKey
40-
value: AlertGroupCustomLabelValue
39+
# AlertGroupCustomLabel represents Alert group custom label for API requests
40+
# Key is just a LabelKey from label repo, while value could be value from repo or a jinja template.
41+
class AlertGroupCustomLabelAPI(typing.TypedDict):
42+
key: LabelKey
43+
value: AlertGroupCustomLabelValueAPI
4144

4245

43-
AlertGroupCustomLabels = list[AlertGroupCustomLabel]
46+
AlertGroupCustomLabelsAPI = list[AlertGroupCustomLabelAPI]
4447

4548

4649
class IntegrationAlertGroupLabels(typing.TypedDict):
4750
inheritable: dict[str, bool]
48-
custom: AlertGroupCustomLabels
51+
custom: AlertGroupCustomLabelsAPI
4952
template: str | None
5053

5154

5255
class CustomLabelSerializer(serializers.Serializer):
53-
"""This serializer is consistent with apps.api.serializers.labels.LabelSerializer, but allows null for value ID."""
56+
"""This serializer is consistent with apps.api.serializers.labels.LabelPairSerializer, but allows null for value ID."""
5457

5558
class CustomLabelKeySerializer(serializers.Serializer):
5659
id = serializers.CharField()
5760
name = serializers.CharField()
61+
prescribed = serializers.BooleanField(default=False)
5862

5963
class CustomLabelValueSerializer(serializers.Serializer):
6064
# ID is null for templated labels. For such labels, the "name" value is a Jinja2 template.
6165
id = serializers.CharField(allow_null=True)
6266
name = serializers.CharField()
67+
prescribed = serializers.BooleanField(default=False)
6368

6469
key = CustomLabelKeySerializer()
6570
value = CustomLabelValueSerializer()
@@ -112,16 +117,26 @@ def update(
112117
return instance
113118

114119
@staticmethod
115-
def _create_custom_labels(organization: Organization, labels: AlertGroupCustomLabels) -> None:
120+
def _create_custom_labels(organization: Organization, labels: AlertGroupCustomLabelsAPI) -> None:
116121
"""Create LabelKeyCache and LabelValueCache objects for custom labels."""
117122

118123
label_keys = [
119-
LabelKeyCache(id=label["key"]["id"], name=label["key"]["name"], organization=organization)
124+
LabelKeyCache(
125+
id=label["key"]["id"],
126+
name=label["key"]["name"],
127+
prescribed=label["key"]["prescribed"],
128+
organization=organization,
129+
)
120130
for label in labels
121131
]
122132

123133
label_values = [
124-
LabelValueCache(id=label["value"]["id"], name=label["value"]["name"], key_id=label["key"]["id"])
134+
LabelValueCache(
135+
id=label["value"]["id"],
136+
name=label["value"]["name"],
137+
prescribed=label["value"]["prescribed"],
138+
key_id=label["key"]["id"],
139+
)
125140
for label in labels
126141
if label["value"]["id"] # don't create LabelValueCache objects for templated labels
127142
]
@@ -147,8 +162,8 @@ def to_representation(cls, instance: AlertReceiveChannel) -> IntegrationAlertGro
147162

148163
@staticmethod
149164
def _custom_labels_to_internal_value(
150-
custom_labels: AlertGroupCustomLabels,
151-
) -> AlertReceiveChannel.AlertGroupCustomLabels:
165+
custom_labels: AlertGroupCustomLabelsAPI,
166+
) -> AlertReceiveChannel.AlertGroupCustomLabelsDB:
152167
"""Convert custom labels from API representation to the schema used by the JSONField on the model."""
153168

154169
return [
@@ -158,8 +173,8 @@ def _custom_labels_to_internal_value(
158173

159174
@staticmethod
160175
def _custom_labels_to_representation(
161-
custom_labels: AlertReceiveChannel.AlertGroupCustomLabels,
162-
) -> AlertGroupCustomLabels:
176+
custom_labels: AlertReceiveChannel.AlertGroupCustomLabelsDB,
177+
) -> AlertGroupCustomLabelsAPI:
163178
"""
164179
Inverse of the _custom_labels_to_internal_value method above.
165180
Fetches label names from DB cache, so the API response schema is consistent with other label endpoints.
@@ -170,33 +185,37 @@ def _custom_labels_to_representation(
170185
if custom_labels is None:
171186
return []
172187

173-
# get up-to-date label key names
174-
label_key_names = {
175-
k.id: k.name
176-
for k in LabelKeyCache.objects.filter(id__in=[label[0] for label in custom_labels]).only("id", "name")
188+
# build index of keys id to name and prescribed flag
189+
label_key_index = {
190+
k.id: {"name": k.name, "prescribed": k.prescribed}
191+
for k in LabelKeyCache.objects.filter(id__in=[label[0] for label in custom_labels]).only(
192+
"id", "name", "prescribed"
193+
)
177194
}
178195

179-
# get up-to-date label value names
180-
label_value_names = {
181-
v.id: v.name
196+
# build index of values id to name and prescribed flag
197+
label_value_index = {
198+
v.id: {"name": v.name, "prescribed": v.prescribed}
182199
for v in LabelValueCache.objects.filter(id__in=[label[1] for label in custom_labels if label[1]]).only(
183-
"id", "name"
200+
"id", "name", "prescribed"
184201
)
185202
}
186203

187204
return [
188205
{
189206
"key": {
190207
"id": key_id,
191-
"name": label_key_names[key_id],
208+
"name": label_key_index[key_id]["name"],
209+
"prescribed": label_key_index[key_id]["prescribed"],
192210
},
193211
"value": {
194212
"id": value_id if value_id else None,
195-
"name": label_value_names[value_id] if value_id else typing.cast(str, template),
213+
"name": label_value_index[value_id]["name"] if value_id else typing.cast(str, template),
214+
"prescribed": label_value_index[value_id]["prescribed"] if value_id else False,
196215
},
197216
}
198217
for key_id, value_id, template in custom_labels
199-
if key_id in label_key_names and (value_id in label_value_names or not value_id)
218+
if key_id in label_key_index and (value_id in label_value_index or not value_id)
200219
]
201220

202221

engine/apps/api/serializers/labels.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,36 @@
66

77
class LabelKeySerializer(serializers.ModelSerializer):
88
id = serializers.CharField()
9+
prescribed = serializers.BooleanField(default=False)
910

1011
class Meta:
1112
model = LabelKeyCache
1213
fields = (
1314
"id",
1415
"name",
16+
"prescribed",
1517
)
1618

1719

1820
class LabelValueSerializer(serializers.ModelSerializer):
1921
id = serializers.CharField()
22+
prescribed = serializers.BooleanField(default=False)
2023

2124
class Meta:
2225
model = LabelValueCache
2326
fields = (
2427
"id",
2528
"name",
29+
"prescribed",
2630
)
2731

2832

29-
class LabelSerializer(serializers.Serializer):
33+
class LabelPairSerializer(serializers.Serializer):
3034
key = LabelKeySerializer()
3135
value = LabelValueSerializer()
3236

3337

34-
class LabelKeyValuesSerializer(serializers.Serializer):
38+
class LabelOptionSerializer(serializers.Serializer):
3539
key = LabelKeySerializer()
3640
values = LabelValueSerializer(many=True)
3741

@@ -41,7 +45,7 @@ class LabelReprSerializer(serializers.Serializer):
4145

4246

4347
class LabelsSerializerMixin(serializers.Serializer):
44-
labels = LabelSerializer(many=True, required=False)
48+
labels = LabelPairSerializer(many=True, required=False)
4549

4650
def validate_labels(self, labels):
4751
if labels:

0 commit comments

Comments
 (0)