Skip to content
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

Support prescribed labels #3848

Merged
merged 48 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
4a39949
fix deps list in rotation form
Feb 1, 2024
cf64b24
Merge branch 'dev' of github.com:grafana/oncall into dev
Feb 2, 2024
4dae491
Cleanup label typing and split update_labels_cache task to not to con…
Konstantinov-Innokentii Feb 7, 2024
93eaa75
Fix typing
Konstantinov-Innokentii Feb 7, 2024
949b4e9
Fix test_get_update_key_get
Konstantinov-Innokentii Feb 7, 2024
d64c5eb
Fix calling update_label_option_cache in update_instances_labels_cache
Konstantinov-Innokentii Feb 7, 2024
a016169
Fix tests
Konstantinov-Innokentii Feb 7, 2024
e0f24d2
Fix tests
Konstantinov-Innokentii Feb 7, 2024
5bffa95
Update new tasks to handle prescribed field
Konstantinov-Innokentii Feb 7, 2024
12044db
Fix test_update_instances_labels_cache_outdated
Konstantinov-Innokentii Feb 7, 2024
00a72fd
Fix task routing
Konstantinov-Innokentii Feb 7, 2024
d6a4ff2
Merge branch 'dev' of github.com:grafana/oncall into dev
Feb 7, 2024
5b5c256
Merge branch 'dev' into prescribed-labels
Konstantinov-Innokentii Feb 7, 2024
b19053e
Update prescribed field
Konstantinov-Innokentii Feb 8, 2024
82b5090
Merge remote-tracking branch 'origin/prescribed-labels' into prescrib…
Konstantinov-Innokentii Feb 8, 2024
128f297
labels split to groups (prescribed/custom)
Feb 9, 2024
cbcbc6a
Update engine/apps/labels/tasks.py
Konstantinov-Innokentii Feb 13, 2024
a0340d9
Update engine/apps/labels/models.py
Konstantinov-Innokentii Feb 13, 2024
8317cd4
Add "prescribed" field to serializers
Konstantinov-Innokentii Feb 13, 2024
3a1b517
Add "prescribed" field to serializers
Konstantinov-Innokentii Feb 13, 2024
0b01fb1
Merge branch 'dev' into prescribed-labels
Konstantinov-Innokentii Feb 13, 2024
0ec1cd7
Remove unnecessary comparision of old and new label data
Konstantinov-Innokentii Feb 13, 2024
53f869e
Update CHANGELOG.md
Konstantinov-Innokentii Feb 13, 2024
8224b4d
Fix tests
Konstantinov-Innokentii Feb 13, 2024
4b2e3ab
Fix tests
Konstantinov-Innokentii Feb 13, 2024
e63e22e
Rename Labels to AlertLabels
Konstantinov-Innokentii Feb 13, 2024
0b9b674
add built-in service labels dropdown
Feb 13, 2024
9e72e55
move splitToGroups to the models/label
Feb 13, 2024
f8a3fbd
make prescribed values non editable
Feb 13, 2024
3a72579
Add prescribed field to AlertGroup custom labels
Konstantinov-Innokentii Feb 14, 2024
7e9097f
Merge branch 'maxim/add-grouping-to-the-labels-dropdown' into prescri…
Konstantinov-Innokentii Feb 14, 2024
9166478
Remove hardcoded prescribed label
Konstantinov-Innokentii Feb 14, 2024
269a40f
Fix imports
Konstantinov-Innokentii Feb 14, 2024
11a9a47
Fix return of the custom labels
Konstantinov-Innokentii Feb 16, 2024
2011232
One more fix return of the custom labels
Konstantinov-Innokentii Feb 16, 2024
d930e48
make prescribed labels non editable 2
Feb 16, 2024
201107b
add isKeyEditable, isValueEditable props to the ServiceLabels component
Feb 16, 2024
4b7f708
fix minor
Feb 16, 2024
58eaa56
Merge branch 'dev' into prescribed-labels
Konstantinov-Innokentii Feb 19, 2024
17d6eea
Merge branch 'dev' into prescribed-labels
Konstantinov-Innokentii Feb 19, 2024
a3e59e3
use ServiceLabels from the npm
Feb 19, 2024
168c7c1
Merge branch 'prescribed-labels' of github.com:grafana/oncall into pr…
Feb 19, 2024
f2e6c22
Fix tests
Konstantinov-Innokentii Feb 20, 2024
c49050e
Fix tests
Konstantinov-Innokentii Feb 20, 2024
6ee28a0
Merge branch 'dev' into prescribed-labels
Konstantinov-Innokentii Feb 20, 2024
6ca4334
Fix tests
Konstantinov-Innokentii Feb 20, 2024
8dcba65
Merge remote-tracking branch 'origin/prescribed-labels' into prescrib…
Konstantinov-Innokentii Feb 20, 2024
bf037e9
Fix tests
Konstantinov-Innokentii Feb 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

– Support prescribed labels ([#3848](https://github.com/grafana/oncall/pull/3848))

### Fixed

- Quotes in templates not rendering results correctly ([#3884](https://github.com/grafana/oncall/pull/3884))
Expand Down
6 changes: 3 additions & 3 deletions engine/apps/alerts/models/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from apps.alerts.signals import alert_group_escalation_snapshot_built
from apps.alerts.tasks.distribute_alert import send_alert_create_signal
from apps.labels.alert_group_labels import assign_labels, gather_labels_from_alert_receive_channel_and_raw_request_data
from apps.labels.types import Labels
from apps.labels.types import AlertLabels
from common.jinja_templater import apply_jinja_template_to_alert_payload_and_labels
from common.jinja_templater.apply_jinja_template import (
JinjaTemplateError,
Expand Down Expand Up @@ -221,7 +221,7 @@ def _apply_jinja_template_to_alert_payload_and_labels(
template_name: str,
alert_receive_channel: "AlertReceiveChannel",
raw_request_data: RawRequestData,
labels: typing.Optional[Labels],
labels: typing.Optional[AlertLabels],
use_error_msg_as_fallback=False,
check_if_templated_value_is_truthy=False,
) -> typing.Union[str, None, bool]:
Expand All @@ -246,7 +246,7 @@ def render_group_data(
cls,
alert_receive_channel: "AlertReceiveChannel",
raw_request_data: RawRequestData,
labels: typing.Optional[Labels],
labels: typing.Optional[AlertLabels],
is_demo=False,
) -> "AlertGroup.GroupData":
from apps.alerts.models import AlertGroup
Expand Down
4 changes: 2 additions & 2 deletions engine/apps/alerts/models/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject):
rate_limited_in_slack_at = models.DateTimeField(null=True, default=None)
rate_limit_message_task_id = models.CharField(max_length=100, null=True, default=None)

AlertGroupCustomLabels = list[tuple[str, str | None, str | None]] | None
alert_group_labels_custom: AlertGroupCustomLabels = models.JSONField(null=True, default=None)
AlertGroupCustomLabelsDB = list[tuple[str, str | None, str | None]] | None
alert_group_labels_custom: AlertGroupCustomLabelsDB = models.JSONField(null=True, default=None)
"""
Stores "custom labels" for alert group labels. Custom labels can be either "plain" or "templated".
For plain labels, the format is: [<LABEL_KEY_ID>, <LABEL_VALUE_ID>, None]
Expand Down
6 changes: 3 additions & 3 deletions engine/apps/alerts/models/channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from django.db.models.manager import RelatedManager

from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel
from apps.labels.types import Labels
from apps.labels.types import AlertLabels

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -120,12 +120,12 @@ def select_filter(
return None

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

def check_filter(
self, raw_request_data: "Alert.RawRequestData", alert_labels: typing.Optional["Labels"] = None
self, raw_request_data: "Alert.RawRequestData", alert_labels: typing.Optional["AlertLabels"] = None
) -> bool:
if self.filtering_term_type == ChannelFilter.FILTERING_TERM_TYPE_JINJA2:
try:
Expand Down
79 changes: 49 additions & 30 deletions engine/apps/api/serializers/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from apps.base.messaging import get_messaging_backends
from apps.integrations.legacy_prefix import has_legacy_prefix
from apps.labels.models import LabelKeyCache, LabelValueCache
from apps.labels.types import LabelKey
from apps.user_management.models import Organization
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.exceptions import BadRequest
Expand All @@ -25,41 +26,45 @@
from .labels import LabelsSerializerMixin


class AlertGroupCustomLabelKey(typing.TypedDict):
id: str
name: str


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


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


AlertGroupCustomLabels = list[AlertGroupCustomLabel]
AlertGroupCustomLabelsAPI = list[AlertGroupCustomLabelAPI]


class IntegrationAlertGroupLabels(typing.TypedDict):
inheritable: dict[str, bool]
custom: AlertGroupCustomLabels
custom: AlertGroupCustomLabelsAPI
template: str | None


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

class CustomLabelKeySerializer(serializers.Serializer):
id = serializers.CharField()
name = serializers.CharField()
prescribed = serializers.BooleanField(required=False, default=False)

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

key = CustomLabelKeySerializer()
value = CustomLabelValueSerializer()
Expand Down Expand Up @@ -112,16 +117,26 @@ def update(
return instance

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

label_keys = [
LabelKeyCache(id=label["key"]["id"], name=label["key"]["name"], organization=organization)
LabelKeyCache(
id=label["key"]["id"],
name=label["key"]["name"],
prescribed=label["key"]["prescribed"],
organization=organization,
)
for label in labels
]

label_values = [
LabelValueCache(id=label["value"]["id"], name=label["value"]["name"], key_id=label["key"]["id"])
LabelValueCache(
id=label["value"]["id"],
name=label["value"]["name"],
prescribed=label["value"]["prescribed"],
key_id=label["key"]["id"],
)
for label in labels
if label["value"]["id"] # don't create LabelValueCache objects for templated labels
]
Expand All @@ -147,8 +162,8 @@ def to_representation(cls, instance: AlertReceiveChannel) -> IntegrationAlertGro

@staticmethod
def _custom_labels_to_internal_value(
custom_labels: AlertGroupCustomLabels,
) -> AlertReceiveChannel.AlertGroupCustomLabels:
custom_labels: AlertGroupCustomLabelsAPI,
) -> AlertReceiveChannel.AlertGroupCustomLabelsDB:
"""Convert custom labels from API representation to the schema used by the JSONField on the model."""

return [
Expand All @@ -158,8 +173,8 @@ def _custom_labels_to_internal_value(

@staticmethod
def _custom_labels_to_representation(
custom_labels: AlertReceiveChannel.AlertGroupCustomLabels,
) -> AlertGroupCustomLabels:
custom_labels: AlertReceiveChannel.AlertGroupCustomLabelsDB,
) -> AlertGroupCustomLabelsAPI:
"""
Inverse of the _custom_labels_to_internal_value method above.
Fetches label names from DB cache, so the API response schema is consistent with other label endpoints.
Expand All @@ -170,33 +185,37 @@ def _custom_labels_to_representation(
if custom_labels is None:
return []

# get up-to-date label key names
label_key_names = {
k.id: k.name
for k in LabelKeyCache.objects.filter(id__in=[label[0] for label in custom_labels]).only("id", "name")
# build index of keys id to name and prescribed flag
label_key_index = {
k.id: {"name": k.name, "prescribed": k.prescribed}
for k in LabelKeyCache.objects.filter(id__in=[label[0] for label in custom_labels]).only(
"id", "name", "prescribed"
)
}

# get up-to-date label value names
label_value_names = {
v.id: v.name
# build index of values id to name and prescribed flag
label_value_index = {
v.id: {"name": v.name, "prescribed": v.prescribed}
for v in LabelValueCache.objects.filter(id__in=[label[1] for label in custom_labels if label[1]]).only(
"id", "name"
"id", "name", "prescribed"
)
}

return [
{
"key": {
"id": key_id,
"name": label_key_names[key_id],
"name": label_key_index[key_id]["name"],
"prescribed": label_key_index[key_id]["prescribed"],
},
"value": {
"id": value_id if value_id else None,
"name": label_value_names[value_id] if value_id else typing.cast(str, template),
"name": label_value_index[value_id]["name"] if value_id else typing.cast(str, template),
"prescribed": label_value_index[value_id]["prescribed"] if value_id else False,
},
}
for key_id, value_id, template in custom_labels
if key_id in label_key_names and (value_id in label_value_names or not value_id)
if key_id in label_key_index and (value_id in label_value_index or not value_id)
]


Expand Down
10 changes: 7 additions & 3 deletions engine/apps/api/serializers/labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,36 @@

class LabelKeySerializer(serializers.ModelSerializer):
id = serializers.CharField()
prescribed = serializers.BooleanField(default=False, required=False)

class Meta:
model = LabelKeyCache
fields = (
"id",
"name",
"prescribed",
)


class LabelValueSerializer(serializers.ModelSerializer):
id = serializers.CharField()
prescribed = serializers.BooleanField(default=False, required=False)

class Meta:
model = LabelValueCache
fields = (
"id",
"name",
"prescribed",
)


class LabelSerializer(serializers.Serializer):
class LabelPairSerializer(serializers.Serializer):
key = LabelKeySerializer()
value = LabelValueSerializer()


class LabelKeyValuesSerializer(serializers.Serializer):
class LabelOptionSerializer(serializers.Serializer):
key = LabelKeySerializer()
values = LabelValueSerializer(many=True)

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


class LabelsSerializerMixin(serializers.Serializer):
labels = LabelSerializer(many=True, required=False)
labels = LabelPairSerializer(many=True, required=False)

def validate_labels(self, labels):
if labels:
Expand Down
60 changes: 59 additions & 1 deletion engine/apps/api/tests/test_alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,59 @@ def test_update_alert_receive_channel_labels(
assert alert_receive_channel.labels.count() == 0


@pytest.mark.django_db
def test_update_alert_receive_channel_presribed_labels(
make_organization_and_user_with_plugin_token,
make_alert_receive_channel,
make_user_auth_headers,
):
organization, user, token = make_organization_and_user_with_plugin_token()
alert_receive_channel = make_alert_receive_channel(organization)
client = APIClient()

url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": alert_receive_channel.public_primary_key})
key_id = "testkey"
value_id = "testvalue"
data = {
"labels": [
{
"key": {"id": key_id, "name": "test", "prescribed": True},
"value": {"id": value_id, "name": "testv", "prescribed": True},
}
]
}
response = client.patch(
url,
data=json.dumps(data),
content_type="application/json",
**make_user_auth_headers(user, token),
)

alert_receive_channel.refresh_from_db()

assert response.status_code == status.HTTP_200_OK
assert alert_receive_channel.labels.count() == 1
label = alert_receive_channel.labels.first()
assert label.key_id == key_id
assert label.value_id == value_id

# Check if cached labels are prescribed
assert label.key.prescribed is True
assert label.value.prescribed is True

response = client.patch(
url,
data=json.dumps({"labels": []}),
content_type="application/json",
**make_user_auth_headers(user, token),
)

alert_receive_channel.refresh_from_db()

assert response.status_code == status.HTTP_200_OK
assert alert_receive_channel.labels.count() == 0


@pytest.mark.django_db
def test_update_alert_receive_channel_labels_duplicate_key(
make_organization_and_user_with_plugin_token,
Expand Down Expand Up @@ -1576,7 +1629,12 @@ def test_alert_group_labels_post(alert_receive_channel_internal_api_setup, make_
labels = [{"key": {"id": "test", "name": "test"}, "value": {"id": "123", "name": "123"}}]
alert_group_labels = {
"inheritable": {"test": False},
"custom": [{"key": {"id": "test", "name": "test"}, "value": {"id": "123", "name": "123"}}],
"custom": [
{
"key": {"id": "test", "name": "test", "prescribed": False},
"value": {"id": "123", "name": "123", "prescribed": False},
}
],
"template": "{{ payload.labels | tojson }}",
}
data = {
Expand Down
6 changes: 3 additions & 3 deletions engine/apps/api/tests/test_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ def test_labels_get_keys(


@patch(
"apps.labels.client.LabelsAPIClient.get_values",
"apps.labels.client.LabelsAPIClient.get_label_by_key_id",
return_value=(
{"key": {"id": "keyid123", "name": "team"}, "values": [{"id": "valueid123", "name": "yolo"}]},
MockResponse(status_code=200),
),
)
@pytest.mark.django_db
def test_get_update_key_get(
mocked_get_values,
mocked_get_label_by_key_id,
make_organization_and_user_with_plugin_token,
make_user_auth_headers,
):
Expand All @@ -55,7 +55,7 @@ def test_get_update_key_get(
response = client.get(url, format="json", **make_user_auth_headers(user, token))
expected_result = {"key": {"id": "keyid123", "name": "team"}, "values": [{"id": "valueid123", "name": "yolo"}]}

assert mocked_get_values.called
assert mocked_get_label_by_key_id.called
assert response.status_code == status.HTTP_200_OK
assert response.json() == expected_result

Expand Down
Loading
Loading