oncall-engine/engine/apps/labels/models.py
Vadim Stepanov 94e2a8472d
Alert group labels filter (#3238)
# What this PR does

Adds a model for alert group labels and adds filtering functionality for
labels on the alert groups page.

## Which issue(s) this PR fixes

https://github.com/grafana/oncall-private/issues/2178

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] 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 <maxim.mordasov@grafana.com>
2023-11-06 10:31:12 +00:00

138 lines
5.8 KiB
Python

import typing
from django.db import models
from django.utils import timezone
from apps.labels.tasks import update_labels_cache
from apps.labels.utils import LABEL_OUTDATED_TIMEOUT_MINUTES, LabelsData
if typing.TYPE_CHECKING:
from apps.user_management.models import Organization
class LabelKeyCache(models.Model):
id = models.CharField(primary_key=True, editable=False, max_length=36)
name = models.CharField(max_length=200)
organization = models.ForeignKey("user_management.Organization", on_delete=models.CASCADE)
last_synced = models.DateTimeField(auto_now=True)
@property
def is_outdated(self) -> bool:
return timezone.now() - self.last_synced > timezone.timedelta(minutes=LABEL_OUTDATED_TIMEOUT_MINUTES)
class LabelValueCache(models.Model):
id = models.CharField(primary_key=True, editable=False, max_length=36)
name = models.CharField(max_length=200)
key = models.ForeignKey("labels.LabelKeyCache", on_delete=models.CASCADE, related_name="values")
last_synced = models.DateTimeField(auto_now=True)
@property
def is_outdated(self) -> bool:
return timezone.now() - self.last_synced > timezone.timedelta(minutes=LABEL_OUTDATED_TIMEOUT_MINUTES)
class AssociatedLabel(models.Model):
"""
Abstract model, is used to keep information about label association with other instances
(integrations, schedules, etc.). To add ability to associate labels with a type of instances ,
inhere this model and add a foreign key to the instance model.
Attention: add `AssociatedLabel` to the end of the name of inheritor (example: AlertReceiveChannelAssociatedLabel)
"""
key = models.ForeignKey(LabelKeyCache, on_delete=models.CASCADE)
value = models.ForeignKey(LabelValueCache, on_delete=models.CASCADE)
organization = models.ForeignKey("user_management.Organization", on_delete=models.CASCADE, related_name="labels")
class Meta:
abstract = True
@staticmethod
def update_association(labels_data: "LabelsData", instance: models.Model, organization: "Organization") -> None:
"""
Update label associations for selected instance: delete associations with labels that are not in `labels_data`,
create new associations and labels, if needed.
Then call celery task to update cache for labels from `labels_data`
instance: the model instance that the labels are associated with (e.g. AlertReceiveChannel instance)
"""
labels_data_keys = {label["key"]["id"]: label["key"]["name"] for label in labels_data}
labels_data_values = {label["value"]["id"]: label["value"]["name"] for label in labels_data}
# delete associations with labels that are not presented in labels_data
instance.labels.exclude(key_id__in=labels_data_keys.keys(), value_id__in=labels_data_values.keys()).delete()
labels_keys = []
labels_values = []
labels_associations = []
for label_data in labels_data:
key_id = label_data["key"]["id"]
key_name = label_data["key"]["name"]
value_id = label_data["value"]["id"]
value_name = label_data["value"]["name"]
label_key = LabelKeyCache(id=key_id, name=key_name, organization=organization)
labels_keys.append(label_key)
label_value = LabelValueCache(id=value_id, name=value_name, key_id=key_id)
labels_values.append(label_value)
associated_instance = {instance.labels.field.name: instance}
labels_associations.append(
instance.labels.model(
key_id=key_id, value_id=value_id, organization=organization, **associated_instance
)
)
# create labels cache and associations that don't exist
LabelKeyCache.objects.bulk_create(labels_keys, ignore_conflicts=True, batch_size=5000)
LabelValueCache.objects.bulk_create(labels_values, ignore_conflicts=True, batch_size=5000)
instance.labels.model.objects.bulk_create(labels_associations, ignore_conflicts=True, batch_size=5000)
update_labels_cache.apply_async((labels_data,))
@staticmethod
def get_associating_label_field_name() -> str:
"""Returns ForeignKey field name for the associated model"""
raise NotImplementedError
class AlertReceiveChannelAssociatedLabel(AssociatedLabel):
"""Keeps information about label association with alert receive channel instances"""
alert_receive_channel = models.ForeignKey(
"alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="labels"
)
class Meta:
unique_together = ["key_id", "value_id", "alert_receive_channel_id"]
@staticmethod
def get_associating_label_field_name() -> str:
"""Returns ForeignKey field name for the associated model"""
return "alert_receive_channel"
class AlertGroupAssociatedLabel(models.Model):
"""
A model for alert group labels (similar to AlertReceiveChannelAssociatedLabel for integrations).
The key difference is that alert group labels do not use label IDs, but rather key and value names explicitly.
This is done to make alert group labels "static" (so they don't change when the labels are updated in label repo).
"""
alert_group = models.ForeignKey("alerts.AlertGroup", on_delete=models.CASCADE, related_name="labels")
organization = models.ForeignKey(
"user_management.Organization", on_delete=models.CASCADE, related_name="alert_group_labels"
)
key_name = models.CharField(max_length=200)
value_name = models.CharField(max_length=200)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["organization", "key_name", "value_name", "alert_group"],
name="unique_alert_group_label",
)
]