From fb1058110147705739228c2339ddf30ade6808d7 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Wed, 4 Dec 2024 12:32:04 +0800 Subject: [PATCH 1/8] Draft custom fields models --- .../0072_alertreceivechannel_custom_fields.py | 19 ++++++++++ .../alerts/models/alert_receive_channel.py | 2 + ...7_customfield_integrationhascustomfield.py | 38 +++++++++++++++++++ engine/apps/labels/models.py | 21 ++++++++++ 4 files changed, 80 insertions(+) create mode 100644 engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py create mode 100644 engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py diff --git a/engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py b/engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py new file mode 100644 index 0000000000..d189885a6a --- /dev/null +++ b/engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.11 on 2024-12-04 04:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('labels', '0007_customfield_integrationhascustomfield'), + ('alerts', '0071_migrate_labels'), + ] + + operations = [ + migrations.AddField( + model_name='alertreceivechannel', + name='custom_fields', + field=models.ManyToManyField(through='labels.IntegrationHasCustomField', to='labels.customfield'), + ), + ] diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index f4661a3a10..1c9d18b9e8 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -314,6 +314,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): additional_settings: dict | None = models.JSONField(null=True, default=None) + custom_fields = models.ManyToManyField("labels.CustomField", through="labels.IntegrationHasCustomField") + class Meta: constraints = [ # This constraint ensures that there's at most one active direct paging integration per team diff --git a/engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py b/engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py new file mode 100644 index 0000000000..d212e1e173 --- /dev/null +++ b/engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.11 on 2024-12-04 04:30 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0071_migrate_labels'), + ('user_management', '0029_remove_organization_general_log_channel_id_db'), + ('labels', '0006_remove_alertreceivechannelassociatedlabel_inheritable_state'), + ] + + operations = [ + migrations.CreateModel( + name='CustomField', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('metaname', models.CharField(max_length=200)), + ('spec', models.JSONField()), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='user_management.organization')), + ], + ), + migrations.CreateModel( + name='IntegrationHasCustomField', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('template', models.TextField(default=None, null=True)), + ('static_value', models.CharField(default=None, max_length=200, null=True)), + ('custom_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labels.customfield')), + ('integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='alerts.alertreceivechannel')), + ], + options={ + 'unique_together': {('integration', 'custom_field')}, + }, + ), + ] diff --git a/engine/apps/labels/models.py b/engine/apps/labels/models.py index ecd06c268d..e7b920b7f4 100644 --- a/engine/apps/labels/models.py +++ b/engine/apps/labels/models.py @@ -170,3 +170,24 @@ class Meta: def get_associating_label_field_name() -> str: """Returns ForeignKey field name for the associated model""" return "webhook" + + +class CustomField(models.Model): + organization = models.ForeignKey( + "user_management.Organization", on_delete=models.CASCADE, related_name="custom_fields" + ) + metaname = models.CharField(max_length=200) + spec = models.JSONField() + + +class IntegrationHasCustomField(models.Model): + integration = models.ForeignKey("alerts.AlertReceiveChannel", on_delete=models.CASCADE) + custom_field = models.ForeignKey(CustomField, on_delete=models.CASCADE) + + # template to parse dynamic value of a custom field + template = models.TextField(null=True, default=None) + # static value is an identifier of selected option. Supposed to be its id, but it's hackathon, right? + static_value = models.CharField(null=True, default=None, max_length=200) + + class Meta: + unique_together = ["integration", "custom_field"] From 3780529e97cc4acb51e56d41bd88a272932f4685 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Wed, 4 Dec 2024 14:12:55 +0800 Subject: [PATCH 2/8] Parse custom fields & rework models --- ...72_alertgroup_custom_fields_customfield.py | 33 ++++++++++++++++ .../0072_alertreceivechannel_custom_fields.py | 19 ---------- engine/apps/alerts/models/alert.py | 24 ++++++++++++ engine/apps/alerts/models/alert_group.py | 2 + .../alerts/models/alert_receive_channel.py | 19 +++++++++- engine/apps/api/serializers/alert_group.py | 1 + ...7_customfield_integrationhascustomfield.py | 38 ------------------- engine/apps/labels/models.py | 21 ---------- 8 files changed, 77 insertions(+), 80 deletions(-) create mode 100644 engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py delete mode 100644 engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py delete mode 100644 engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py diff --git a/engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py b/engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py new file mode 100644 index 0000000000..cbe7853e7b --- /dev/null +++ b/engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.11 on 2024-12-04 06:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('alerts', '0071_migrate_labels'), + ] + + operations = [ + migrations.AddField( + model_name='alertgroup', + name='custom_fields', + field=models.JSONField(default=None, null=True), + ), + migrations.CreateModel( + name='CustomField', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('metaname', models.CharField(max_length=200)), + ('spec', models.JSONField()), + ('template', models.TextField(default=None, null=True)), + ('static_value', models.CharField(default=None, max_length=200, null=True)), + ('integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='alerts.alertreceivechannel')), + ], + options={ + 'unique_together': {('integration', 'metaname')}, + }, + ), + ] diff --git a/engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py b/engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py deleted file mode 100644 index d189885a6a..0000000000 --- a/engine/apps/alerts/migrations/0072_alertreceivechannel_custom_fields.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.11 on 2024-12-04 04:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('labels', '0007_customfield_integrationhascustomfield'), - ('alerts', '0071_migrate_labels'), - ] - - operations = [ - migrations.AddField( - model_name='alertreceivechannel', - name='custom_fields', - field=models.ManyToManyField(through='labels.IntegrationHasCustomField', to='labels.customfield'), - ), - ] diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index 844cbf6771..3958c88cad 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -20,6 +20,7 @@ from common.jinja_templater.apply_jinja_template import ( JinjaTemplateError, JinjaTemplateWarning, + apply_jinja_template, templated_value_is_truthy, ) from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length @@ -142,6 +143,8 @@ def create( if group_created: assign_labels(group, alert_receive_channel, parsed_labels) + group.custom_fields = parse_custom_fields(alert_receive_channel, raw_request_data) + group.save(update_fields=["custom_fields"]) group.log_records.create(type=AlertGroupLogRecord.TYPE_REGISTERED) group.log_records.create(type=AlertGroupLogRecord.TYPE_ROUTE_ASSIGNED) @@ -321,3 +324,24 @@ def insert_random_uuid(distinction: typing.Optional[str]) -> str: distinction = str(uuid4()) return distinction + + +# parse_custom_fields parses custom fields from the alert payload. +# It returns a dictionary of custom fields_id:parsed_value +# parsed value supposed to be id of an option, but for sake of simplicity it's a string +def parse_custom_fields( + alert_receive_channel: "AlertReceiveChannel", raw_request_data: "Alert.RawRequestData" +) -> typing.Dict[str, str]: + custom_fields = {} + # parse custom fields + for field in alert_receive_channel.custom_fields.all(): + if field.static_value: + custom_fields[field.metaname] = field.static_value + elif field.template: + try: + custom_fields[field.metaname] = apply_jinja_template(field.template, raw_request_data) + except (JinjaTemplateError, JinjaTemplateWarning) as e: + logger.warning("parse_custom_fields: failed to apply template: %s", e.fallback_message) + continue + + return custom_fields diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index 3db1d9edfb..efae298ef5 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -358,6 +358,8 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. received_at = models.DateTimeField(blank=True, null=True, default=None) + custom_fields = models.JSONField(null=True, default=None) + @property def is_silenced_forever(self): return self.silenced and self.silenced_until is None diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 1c9d18b9e8..391ad50136 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -202,6 +202,7 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): organization: "Organization" team: typing.Optional["Team"] labels: "RelatedManager['AlertReceiveChannelAssociatedLabel']" + custom_fields: "RelatedManager['CustomField']" objects = AlertReceiveChannelManager() objects_with_maintenance = AlertReceiveChannelManagerWithMaintenance() @@ -314,8 +315,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): additional_settings: dict | None = models.JSONField(null=True, default=None) - custom_fields = models.ManyToManyField("labels.CustomField", through="labels.IntegrationHasCustomField") - class Meta: constraints = [ # This constraint ensures that there's at most one active direct paging integration per team @@ -797,3 +796,19 @@ def listen_for_alertreceivechannel_model_save( metrics_remove_deleted_integration_from_cache(instance) else: metrics_update_integration_cache(instance) + + +class CustomField(models.Model): + integration = models.ForeignKey(AlertReceiveChannel, on_delete=models.CASCADE, related_name="custom_fields") + # metadata.name of the custom field + metaname = models.CharField(max_length=200) + # spec of the custom field + spec = models.JSONField() + # template to parse dynamic value of a custom field + template = models.TextField(null=True, default=None) + # static value is an identifier of selected option. + # Probably display value & id of an option should be different, but I merged them for now. + static_value = models.CharField(null=True, default=None, max_length=200) + + class Meta: + unique_together = ["integration", "metaname"] diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index c0882658fb..ea55afd2fa 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -245,6 +245,7 @@ class Meta(AlertGroupListSerializer.Meta): "last_alert_at", "paged_users", "external_urls", + "custom_fields", ] def get_last_alert_at(self, obj: "AlertGroup") -> datetime.datetime: diff --git a/engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py b/engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py deleted file mode 100644 index d212e1e173..0000000000 --- a/engine/apps/labels/migrations/0007_customfield_integrationhascustomfield.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.2.11 on 2024-12-04 04:30 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('alerts', '0071_migrate_labels'), - ('user_management', '0029_remove_organization_general_log_channel_id_db'), - ('labels', '0006_remove_alertreceivechannelassociatedlabel_inheritable_state'), - ] - - operations = [ - migrations.CreateModel( - name='CustomField', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('metaname', models.CharField(max_length=200)), - ('spec', models.JSONField()), - ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='user_management.organization')), - ], - ), - migrations.CreateModel( - name='IntegrationHasCustomField', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('template', models.TextField(default=None, null=True)), - ('static_value', models.CharField(default=None, max_length=200, null=True)), - ('custom_field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labels.customfield')), - ('integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='alerts.alertreceivechannel')), - ], - options={ - 'unique_together': {('integration', 'custom_field')}, - }, - ), - ] diff --git a/engine/apps/labels/models.py b/engine/apps/labels/models.py index e7b920b7f4..ecd06c268d 100644 --- a/engine/apps/labels/models.py +++ b/engine/apps/labels/models.py @@ -170,24 +170,3 @@ class Meta: def get_associating_label_field_name() -> str: """Returns ForeignKey field name for the associated model""" return "webhook" - - -class CustomField(models.Model): - organization = models.ForeignKey( - "user_management.Organization", on_delete=models.CASCADE, related_name="custom_fields" - ) - metaname = models.CharField(max_length=200) - spec = models.JSONField() - - -class IntegrationHasCustomField(models.Model): - integration = models.ForeignKey("alerts.AlertReceiveChannel", on_delete=models.CASCADE) - custom_field = models.ForeignKey(CustomField, on_delete=models.CASCADE) - - # template to parse dynamic value of a custom field - template = models.TextField(null=True, default=None) - # static value is an identifier of selected option. Supposed to be its id, but it's hackathon, right? - static_value = models.CharField(null=True, default=None, max_length=200) - - class Meta: - unique_together = ["integration", "custom_field"] From e5eda35feff44e6785304ed84c712bbd04d462d8 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Wed, 4 Dec 2024 14:38:22 +0800 Subject: [PATCH 3/8] Api for configuring custom fields --- .../api/serializers/alert_receive_channel.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index 9065ca6801..eec6190c29 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -11,6 +11,7 @@ from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager from apps.alerts.models import AlertReceiveChannel +from apps.alerts.models.alert_receive_channel import CustomField from apps.base.messaging import get_messaging_backends from apps.integrations.legacy_prefix import has_legacy_prefix from apps.labels.models import AlertReceiveChannelAssociatedLabel, LabelKeyCache, LabelValueCache @@ -262,6 +263,15 @@ def _custom_labels_to_representation( ] +class CustomFieldSerializer(serializers.ModelSerializer): + class Meta: + model = CustomField + fields = ["metaname", "spec", "template", "static_value"] + extra_kwargs = { + "metaname": {"required": True}, + } + + class AlertReceiveChannelSerializer( EagerLoadingMixin, LabelsSerializerMixin, serializers.ModelSerializer[AlertReceiveChannel] ): @@ -288,6 +298,7 @@ class AlertReceiveChannelSerializer( is_legacy = serializers.SerializerMethodField() alert_group_labels = IntegrationAlertGroupLabelsSerializer(source="*", required=False) additional_settings = AdditionalSettingsField(allow_null=True, allow_empty=False, required=False, default=None) + custom_fields = CustomFieldSerializer(many=True, required=False) # integration heartbeat is in PREFETCH_RELATED not by mistake. # With using of select_related ORM builds strange join @@ -331,6 +342,7 @@ class Meta: "alert_group_labels", "alertmanager_v2_migrated_at", "additional_settings", + "custom_fields", ] read_only_fields = [ "created_at", @@ -413,7 +425,8 @@ def create(self, validated_data): # pop associated labels and alert group labels, so they are not passed to AlertReceiveChannel.create labels = validated_data.pop("labels", None) alert_group_labels = IntegrationAlertGroupLabelsSerializer.pop_alert_group_labels(validated_data) - + # Extract custom fields data + custom_fields_data = validated_data.pop("custom_fields", []) try: instance = AlertReceiveChannel.create( **validated_data, @@ -432,6 +445,10 @@ def create(self, validated_data): if create_default_webhooks and hasattr(instance.config, "create_default_webhooks"): instance.config.create_default_webhooks(instance) + # Create custom fields + for custom_field_data in custom_fields_data: + CustomField.objects.create(integration=instance, **custom_field_data) + return instance def update(self, instance, validated_data): @@ -444,6 +461,28 @@ def update(self, instance, validated_data): instance, IntegrationAlertGroupLabelsSerializer.pop_alert_group_labels(validated_data) ) + # update custom fields + # Extract custom fields data + custom_fields_data = validated_data.pop("custom_fields", []) + # Update custom fields + existing_custom_fields = {cf.metaname: cf for cf in instance.custom_fields.all()} + for custom_field_data in custom_fields_data: + custom_field_name = custom_field_data.get("metaname") + if custom_field_name and custom_field_name in existing_custom_fields: + # Update existing custom field + custom_field_instance = existing_custom_fields[custom_field_name] + for attr, value in custom_field_data.items(): + setattr(custom_field_instance, attr, value) + custom_field_instance.save() + else: + # Create new custom field + CustomField.objects.create(integration=instance, **custom_field_data) + # Delete custom fields not included in the update + provided_custom_fields = {cf.get("metaname") for cf in custom_fields_data} + for custom_field_metaname in existing_custom_fields: + if custom_field_metaname not in provided_custom_fields: + existing_custom_fields[custom_field_metaname].delete() + try: updated_instance = super().update(instance, validated_data) except AlertReceiveChannel.DuplicateDirectPagingError: From ee5900b43376d96c1cfd2d9259dfd74a92efc466 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Wed, 4 Dec 2024 21:16:20 +0800 Subject: [PATCH 4/8] Slightly change field names --- .../migrations/0072_alertgroup_custom_fields_customfield.py | 5 ++--- engine/apps/alerts/models/alert_receive_channel.py | 4 +--- engine/apps/api/serializers/alert_receive_channel.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py b/engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py index cbe7853e7b..2fb928890f 100644 --- a/engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py +++ b/engine/apps/alerts/migrations/0072_alertgroup_custom_fields_customfield.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.11 on 2024-12-04 06:00 +# Generated by Django 4.2.11 on 2024-12-04 13:15 from django.db import migrations, models import django.db.models.deletion @@ -21,8 +21,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('metaname', models.CharField(max_length=200)), - ('spec', models.JSONField()), - ('template', models.TextField(default=None, null=True)), + ('dynamic_template', models.TextField(default=None, null=True)), ('static_value', models.CharField(default=None, max_length=200, null=True)), ('integration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='custom_fields', to='alerts.alertreceivechannel')), ], diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 391ad50136..a00544c6f6 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -802,10 +802,8 @@ class CustomField(models.Model): integration = models.ForeignKey(AlertReceiveChannel, on_delete=models.CASCADE, related_name="custom_fields") # metadata.name of the custom field metaname = models.CharField(max_length=200) - # spec of the custom field - spec = models.JSONField() # template to parse dynamic value of a custom field - template = models.TextField(null=True, default=None) + dynamic_template = models.TextField(null=True, default=None) # static value is an identifier of selected option. # Probably display value & id of an option should be different, but I merged them for now. static_value = models.CharField(null=True, default=None, max_length=200) diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index eec6190c29..deca8db29a 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -266,7 +266,7 @@ def _custom_labels_to_representation( class CustomFieldSerializer(serializers.ModelSerializer): class Meta: model = CustomField - fields = ["metaname", "spec", "template", "static_value"] + fields = ["metaname", "dynamic_template", "static_value"] extra_kwargs = { "metaname": {"required": True}, } From 075baf3b3ab391134647b0b9cae134238296e97c Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Thu, 5 Dec 2024 18:50:37 +0800 Subject: [PATCH 5/8] Store fields as array --- engine/apps/alerts/models/alert.py | 17 ++++++++++------- engine/apps/alerts/models/alert_group.py | 2 ++ .../apps/alerts/models/alert_receive_channel.py | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index 3958c88cad..e934908867 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -328,20 +328,23 @@ def insert_random_uuid(distinction: typing.Optional[str]) -> str: # parse_custom_fields parses custom fields from the alert payload. # It returns a dictionary of custom fields_id:parsed_value -# parsed value supposed to be id of an option, but for sake of simplicity it's a string def parse_custom_fields( alert_receive_channel: "AlertReceiveChannel", raw_request_data: "Alert.RawRequestData" -) -> typing.Dict[str, str]: - custom_fields = {} +) -> typing.List: + fields = [] # parse custom fields for field in alert_receive_channel.custom_fields.all(): if field.static_value: - custom_fields[field.metaname] = field.static_value + f = {"metaname": field.metaname, "static_value": field.static_value} + fields.append(field) elif field.template: try: - custom_fields[field.metaname] = apply_jinja_template(field.template, raw_request_data) + result = apply_jinja_template(field.template, raw_request_data) + f = {"metaname": field.metaname, "value": result} + if result: + fields.append(f) except (JinjaTemplateError, JinjaTemplateWarning) as e: logger.warning("parse_custom_fields: failed to apply template: %s", e.fallback_message) continue - - return custom_fields + fields.sort(key=lambda x: x["metaname"]) + return fields diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index efae298ef5..bbf3fe032c 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -358,6 +358,8 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models. received_at = models.DateTimeField(blank=True, null=True, default=None) + # custom_fields is a dict of custom fields applied to the group. + # currently it does not support referral integrity, storing just string repr of a custom field key & value custom_fields = models.JSONField(null=True, default=None) @property diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index a00544c6f6..6d84cfaf16 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -805,7 +805,7 @@ class CustomField(models.Model): # template to parse dynamic value of a custom field dynamic_template = models.TextField(null=True, default=None) # static value is an identifier of selected option. - # Probably display value & id of an option should be different, but I merged them for now. + # Probably static_value should be split into the ID & display_name, but it's merged for sake of hackathon static_value = models.CharField(null=True, default=None, max_length=200) class Meta: From 4a3a176acb6e3ff5ae0e0c0b4a20e383e1b7f911 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Thu, 5 Dec 2024 18:57:00 +0800 Subject: [PATCH 6/8] Fix --- engine/apps/alerts/models/alert.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index e934908867..6623644507 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -333,14 +333,14 @@ def parse_custom_fields( ) -> typing.List: fields = [] # parse custom fields - for field in alert_receive_channel.custom_fields.all(): - if field.static_value: - f = {"metaname": field.metaname, "static_value": field.static_value} - fields.append(field) - elif field.template: + for field_config in alert_receive_channel.custom_fields.all(): + if field_config.static_value: + f = {"metaname": field_config.metaname, "static_value": field_config.static_value} + fields.append(f) + elif field_config.template: try: - result = apply_jinja_template(field.template, raw_request_data) - f = {"metaname": field.metaname, "value": result} + result = apply_jinja_template(field_config.template, raw_request_data) + f = {"metaname": field_config.metaname, "value": result} if result: fields.append(f) except (JinjaTemplateError, JinjaTemplateWarning) as e: From 10ceaf066bcb800772336fb235080c4dc8018a7d Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 6 Dec 2024 14:05:28 +0800 Subject: [PATCH 7/8] Pass custom fields in declare incident link --- engine/apps/alerts/models/alert.py | 4 ++-- engine/apps/alerts/models/alert_group.py | 15 +++++++++++++-- engine/apps/alerts/utils.py | 1 + engine/apps/grafana_plugin/ui_url_builder.py | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index 6623644507..05a744d99b 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -337,9 +337,9 @@ def parse_custom_fields( if field_config.static_value: f = {"metaname": field_config.metaname, "static_value": field_config.static_value} fields.append(f) - elif field_config.template: + elif field_config.dynamic_template: try: - result = apply_jinja_template(field_config.template, raw_request_data) + result = apply_jinja_template(field_config.dynamic_template, raw_request_data) f = {"metaname": field_config.metaname, "value": result} if result: fields.append(f) diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index bbf3fe032c..5b2ecf0b07 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -1,4 +1,5 @@ import datetime +import json import logging import typing import urllib @@ -566,8 +567,18 @@ def declare_incident_link(self) -> str: title = urllib.parse.quote_plus(self.web_title_cache) if self.web_title_cache else DEFAULT_BACKUP_TITLE title = title[:2000] # set max title length to avoid exceptions with too long declare incident link link = urllib.parse.quote_plus(self.web_link) - - return UIURLBuilder(self.channel.organization).declare_incident(f"?caption={caption}&url={link}&title={title}") + params = f"?caption={caption}&url={link}&title={title}" + if self.custom_fields is not None: + print("YOLO") + print(self.custom_fields) + print("BOLO") + print(type(self.custom_fields)) + jsonCustomFields = json.dumps(self.custom_fields) + print("DOLO") + print(jsonCustomFields) + custom_fields = urllib.parse.quote_plus(jsonCustomFields) + params += f"&cf={custom_fields}" + return UIURLBuilder(self.channel.organization).declare_incident(params) @property def happened_while_maintenance(self): diff --git a/engine/apps/alerts/utils.py b/engine/apps/alerts/utils.py index 5317c22b3f..57ab274008 100644 --- a/engine/apps/alerts/utils.py +++ b/engine/apps/alerts/utils.py @@ -23,4 +23,5 @@ def render_relative_timeline(log_created_at, alert_group_started_at): def is_declare_incident_step_enabled(organization: "Organization") -> bool: + return True return organization.is_grafana_incident_enabled and settings.FEATURE_DECLARE_INCIDENT_STEP_ENABLED diff --git a/engine/apps/grafana_plugin/ui_url_builder.py b/engine/apps/grafana_plugin/ui_url_builder.py index e37f8e7542..a1d5a6f5d9 100644 --- a/engine/apps/grafana_plugin/ui_url_builder.py +++ b/engine/apps/grafana_plugin/ui_url_builder.py @@ -13,7 +13,7 @@ class UIURLBuilder: """ def __init__(self, organization: "Organization", base_url: typing.Optional[str] = None) -> None: - self.base_url = base_url if base_url else organization.grafana_url + self.base_url = "http://localhost:3000" self.is_grafana_irm_enabled = organization.is_grafana_irm_enabled def _build_url(self, page: str, path_extra: str = "", plugin_id: typing.Optional[str] = None) -> str: From f844e2db96bf1f5365d491905fa249d57380e46b Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Fri, 6 Dec 2024 15:05:27 +0800 Subject: [PATCH 8/8] Some api fixes --- engine/apps/alerts/models/alert.py | 4 ++-- engine/apps/alerts/models/alert_group.py | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index 05a744d99b..729a11085c 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -335,12 +335,12 @@ def parse_custom_fields( # parse custom fields for field_config in alert_receive_channel.custom_fields.all(): if field_config.static_value: - f = {"metaname": field_config.metaname, "static_value": field_config.static_value} + f = {"name": field_config.metaname, "value": field_config.static_value} fields.append(f) elif field_config.dynamic_template: try: result = apply_jinja_template(field_config.dynamic_template, raw_request_data) - f = {"metaname": field_config.metaname, "value": result} + f = {"name": field_config.metaname, "value": result} if result: fields.append(f) except (JinjaTemplateError, JinjaTemplateWarning) as e: diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index 5b2ecf0b07..a72df48a9d 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -563,18 +563,14 @@ def declare_incident_link(self) -> str: """ Generate a link for AlertGroup to declare Grafana Incident by click """ + print("HELLO") caption = urllib.parse.quote_plus("OnCall Alert Group") title = urllib.parse.quote_plus(self.web_title_cache) if self.web_title_cache else DEFAULT_BACKUP_TITLE title = title[:2000] # set max title length to avoid exceptions with too long declare incident link link = urllib.parse.quote_plus(self.web_link) params = f"?caption={caption}&url={link}&title={title}" if self.custom_fields is not None: - print("YOLO") - print(self.custom_fields) - print("BOLO") - print(type(self.custom_fields)) jsonCustomFields = json.dumps(self.custom_fields) - print("DOLO") print(jsonCustomFields) custom_fields = urllib.parse.quote_plus(jsonCustomFields) params += f"&cf={custom_fields}"