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

IRM schema #5324

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.11 on 2024-12-04 13:15

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)),
('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')),
],
options={
'unique_together': {('integration', 'metaname')},
},
),
]
27 changes: 27 additions & 0 deletions engine/apps/alerts/models/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -321,3 +324,27 @@ 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
def parse_custom_fields(
alert_receive_channel: "AlertReceiveChannel", raw_request_data: "Alert.RawRequestData"
) -> typing.List:
fields = []
# parse custom fields
for field_config in alert_receive_channel.custom_fields.all():
if 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 = {"name": field_config.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
fields.sort(key=lambda x: x["metaname"])
return fields
15 changes: 13 additions & 2 deletions engine/apps/alerts/models/alert_group.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import json
import logging
import typing
import urllib
Expand Down Expand Up @@ -358,6 +359,10 @@ 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
def is_silenced_forever(self):
return self.silenced and self.silenced_until is None
Expand Down Expand Up @@ -558,12 +563,18 @@ 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)

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:
jsonCustomFields = json.dumps(self.custom_fields)
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):
Expand Down
15 changes: 15 additions & 0 deletions engine/apps/alerts/models/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -795,3 +796,17 @@ 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)
# 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 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:
unique_together = ["integration", "metaname"]
1 change: 1 addition & 0 deletions engine/apps/alerts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions engine/apps/api/serializers/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
41 changes: 40 additions & 1 deletion engine/apps/api/serializers/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -262,6 +263,15 @@ def _custom_labels_to_representation(
]


class CustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = CustomField
fields = ["metaname", "dynamic_template", "static_value"]
extra_kwargs = {
"metaname": {"required": True},
}


class AlertReceiveChannelSerializer(
EagerLoadingMixin, LabelsSerializerMixin, serializers.ModelSerializer[AlertReceiveChannel]
):
Expand All @@ -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
Expand Down Expand Up @@ -331,6 +342,7 @@ class Meta:
"alert_group_labels",
"alertmanager_v2_migrated_at",
"additional_settings",
"custom_fields",
]
read_only_fields = [
"created_at",
Expand Down Expand Up @@ -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,
Expand All @@ -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):
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/grafana_plugin/ui_url_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading