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

[WIP] Set team on Alert Group based on route #3459 #5320

Draft
wants to merge 4 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
4 changes: 4 additions & 0 deletions docs/sources/oncall-api-reference/alertgroups.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,17 @@ The above command returns JSON structured in the following way:
]
}
},
"teams": [
"TE5EF3RQHJQPI"
]
}
],
"current_page_number": 1,
"page_size": 50,
"total_pages": 1
}
```
> **Note**: `team_id` is provided for each alert_group however this is based off the old method where team was assigned based on integration team. It's recommended to use the new `teams` field.

> **Note**: The response is [paginated](ref:pagination). You may need to make multiple requests to get all records.

Expand Down
19 changes: 19 additions & 0 deletions engine/apps/alerts/migrations/0065_alertgroup_teams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.16 on 2024-11-19 06:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('user_management', '0026_auto_20241017_1919'),
('alerts', '0064_migrate_resolutionnoteslackmessage_slack_channel_id'),
]

operations = [
migrations.AddField(
model_name='alertgroup',
name='teams',
field=models.ManyToManyField(to='user_management.team'),
),
]
18 changes: 18 additions & 0 deletions engine/apps/alerts/migrations/0066_channelfilter_update_team.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-11-22 00:22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('alerts', '0065_alertgroup_teams'),
]

operations = [
migrations.AddField(
model_name='channelfilter',
name='update_team',
field=models.BooleanField(default=False, null=True),
),
]
12 changes: 12 additions & 0 deletions engine/apps/alerts/models/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ def create(
group.log_records.create(type=AlertGroupLogRecord.TYPE_REGISTERED)
group.log_records.create(type=AlertGroupLogRecord.TYPE_ROUTE_ASSIGNED)

if alert_receive_channel.team:
# add the team from the integration if its available
group.teams.set([alert_receive_channel.team])
elif (
channel_filter
and channel_filter.escalation_chain
and channel_filter.escalation_chain.team
and channel_filter.update_team
):
# set the team to the one defined in the escalation_chain if defined.
group.teams.set([channel_filter.escalation_chain.team])

if group_created or alert.group.pause_escalation:
# Build escalation snapshot if needed and start escalation
alert.group.start_escalation_if_needed(countdown=TASK_DELAY_SECONDS)
Expand Down
2 changes: 2 additions & 0 deletions engine/apps/alerts/models/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ def status(self) -> int:

raw_escalation_snapshot = JSONField(null=True, default=None)

teams = models.ManyToManyField(to="user_management.Team")

# This field is used for constraints so we can use get_or_create() in concurrent calls
# https://docs.djangoproject.com/en/3.2/ref/models/querysets/#get-or-create
# Combined with unique_together below, it allows only one alert group with
Expand Down
3 changes: 3 additions & 0 deletions engine/apps/alerts/models/channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class ChannelFilter(OrderedModel):
"alerts.EscalationChain", null=True, default=None, on_delete=models.SET_NULL, related_name="channel_filters"
)

# Should we update the alertgroup team when this route is used
update_team = models.BooleanField(null=True, default=False)

notify_in_slack = models.BooleanField(null=True, default=True)
notify_in_telegram = models.BooleanField(null=True, default=False)

Expand Down
22 changes: 19 additions & 3 deletions engine/apps/api/serializers/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .alert_receive_channel import FastAlertReceiveChannelSerializer
from .alerts_field_cache_buster_mixin import AlertsFieldCacheBusterMixin
from .user import FastUserSerializer, UserShortSerializer
from .team import FastTeamSerializer

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
Expand Down Expand Up @@ -125,8 +126,7 @@ class AlertGroupListSerializer(
related_users = serializers.SerializerMethodField()
dependent_alert_groups = ShortAlertGroupSerializer(many=True)
root_alert_group = ShortAlertGroupSerializer()
team = TeamPrimaryKeyRelatedField(source="channel.team", allow_null=True)

teams = serializers.SerializerMethodField()
alerts_count = serializers.IntegerField(read_only=True)
render_for_web = serializers.SerializerMethodField()

Expand All @@ -136,6 +136,7 @@ class AlertGroupListSerializer(
"dependent_alert_groups",
"log_records__author",
"labels",
"teams",
Prefetch(
"slack_messages",
queryset=SlackMessage.objects.select_related("_slack_team_identity").order_by("created_at")[:1],
Expand Down Expand Up @@ -187,12 +188,26 @@ class Meta:
"root_alert_group",
"status",
"declare_incident_link",
"team",
"grafana_incident_id",
"labels",
"permalinks",
"teams"
]

@extend_schema_field(FastTeamSerializer(many=True))
def get_teams(self, obj: "AlertGroup"):
"""
Handle AlertGroups that haven't been assigned a team yet
"""

if obj.teams.exists():
teams = obj.teams
elif obj.channel.team:
teams = [obj.channel.team]
else:
teams = []
return FastTeamSerializer(teams, context=self.context, many=True).data

def get_render_for_web(self, obj: "AlertGroup") -> RenderForWeb | EmptyRenderForWeb:
if not obj.last_alert:
return {}
Expand Down Expand Up @@ -231,6 +246,7 @@ def get_related_users(self, obj: "AlertGroup"):
return UserShortSerializer(users, context=self.context, many=True).data



class AlertGroupSerializer(AlertGroupListSerializer):
alerts = serializers.SerializerMethodField("get_limited_alerts")
last_alert_at = serializers.SerializerMethodField()
Expand Down
2 changes: 2 additions & 0 deletions engine/apps/api/serializers/channel_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class Meta:
"notification_backends",
"filtering_term_as_jinja2",
"telegram_channel_details",
"update_team"
]
read_only_fields = [
"created_at",
Expand Down Expand Up @@ -165,6 +166,7 @@ class Meta:
"notify_in_slack",
"notify_in_telegram",
"notification_backends",
"update_team"
]
read_only_fields = ["created_at", "is_default"]

Expand Down
38 changes: 30 additions & 8 deletions engine/apps/api/tests/test_alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from apps.alerts.constants import ActionSource
from apps.alerts.models import (
Alert,
AlertGroup,
AlertGroupExternalID,
AlertGroupLogRecord,
Expand Down Expand Up @@ -887,14 +888,35 @@ def test_get_filter_by_teams(
alert_receive_channel_1 = make_alert_receive_channel(organization, team=team1)
alert_receive_channel_2 = make_alert_receive_channel(organization, team=team2)

alert_group_0 = make_alert_group(alert_receive_channel_0)
make_alert(alert_group=alert_group_0, raw_request_data=alert_raw_request_data)

alert_group_1 = make_alert_group(alert_receive_channel_1)
make_alert(alert_group=alert_group_1, raw_request_data=alert_raw_request_data)

alert_group_2 = make_alert_group(alert_receive_channel_2)
make_alert(alert_group=alert_group_2, raw_request_data=alert_raw_request_data)
alert_group_0 = Alert.create(
title="the title",
message="the message",
alert_receive_channel=alert_receive_channel_0,
raw_request_data={},
integration_unique_data={},
image_url=None,
link_to_upstream_details=None,
).group

alert_group_1 = Alert.create(
title="the title",
message="the message",
alert_receive_channel=alert_receive_channel_1,
raw_request_data={},
integration_unique_data={},
image_url=None,
link_to_upstream_details=None,
).group

alert_group_2 = Alert.create(
title="the title",
message="the message",
alert_receive_channel=alert_receive_channel_2,
raw_request_data={},
integration_unique_data={},
image_url=None,
link_to_upstream_details=None,
).group

url = reverse("api-internal:alertgroup-list")

Expand Down
22 changes: 19 additions & 3 deletions engine/apps/api/tests/test_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest_framework import status
from rest_framework.test import APIClient

from apps.alerts.models import AlertReceiveChannel
from apps.alerts.models import AlertReceiveChannel, Alert
from apps.api.permissions import LegacyAccessControlRole
from apps.api.serializers.user import UserHiddenFieldsSerializer
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar, OnCallScheduleWeb
Expand Down Expand Up @@ -356,7 +356,15 @@ def test_team_permissions_wrong_team(
user.teams.add(team_with_user)

alert_receive_channel = make_alert_receive_channel(organization, team=team_without_user)
alert_group = make_alert_group(alert_receive_channel)
alert_group = Alert.create(
title="the title",
message="the message",
alert_receive_channel=alert_receive_channel,
raw_request_data={},
integration_unique_data={},
image_url=None,
link_to_upstream_details=None,
).group

escalation_chain = make_escalation_chain(organization, team=team_without_user)
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team_without_user)
Expand Down Expand Up @@ -402,7 +410,15 @@ def test_team_permissions_not_in_team(
another_user.save(update_fields=["current_team"])

alert_receive_channel = make_alert_receive_channel(organization, team=team)
alert_group = make_alert_group(alert_receive_channel)
alert_group = Alert.create(
title="the title",
message="the message",
alert_receive_channel=alert_receive_channel,
raw_request_data={},
integration_unique_data={},
image_url=None,
link_to_upstream_details=None,
).group

escalation_chain = make_escalation_chain(organization, team=team)
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team)
Expand Down
41 changes: 32 additions & 9 deletions engine/apps/api/views/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ def filter_with_resolution_note(self, queryset, name, value):
).distinct()
return queryset


# TODO need to probably rework this to ensure it works correctly with both paradigms
class AlertGroupTeamFilteringMixin(TeamFilteringMixin):
TEAM_LOOKUP = "team"
TEAM_LOOKUP = "teams"

def retrieve(self, request, *args, **kwargs):
try:
Expand Down Expand Up @@ -303,23 +303,46 @@ def get_queryset(self, ignore_filtering_by_available_teams=False):
# no select_related or prefetch_related is used at this point, it will be done on paginate_queryset.

alert_receive_channels_qs = AlertReceiveChannel.objects_with_deleted.filter(
organization_id=self.request.auth.organization.id
)
organization_id=self.request.auth.organization.id
)
if not ignore_filtering_by_available_teams:
alert_receive_channels_qs = alert_receive_channels_qs.filter(*self.available_teams_lookup_args)
alert_receive_channels_qs = alert_receive_channels_qs.filter(*self.available_teams_lookup_args_with_field(field="team"))

# Filter by team(s). Since we really filter teams from integrations, this is not an AlertGroup model filter.
# This is based on the common.api_helpers.ByTeamModelFieldFilterMixin implementation

team_values = self.request.query_params.getlist("team", [])

if team_values:
null_team_lookup = Q(team__isnull=True) if NO_TEAM_VALUE in team_values else None
teams_lookup = Q(team__public_primary_key__in=[ppk for ppk in team_values if ppk != NO_TEAM_VALUE])
alert_receive_channels_teams_lookup = Q(team__public_primary_key__in=[ppk for ppk in team_values if ppk != NO_TEAM_VALUE])
if null_team_lookup:
teams_lookup = teams_lookup | null_team_lookup
alert_receive_channels_qs = alert_receive_channels_qs.filter(teams_lookup)
alert_receive_channels_teams_lookup = alert_receive_channels_teams_lookup | null_team_lookup
alert_receive_channels_qs = alert_receive_channels_qs.filter(alert_receive_channels_teams_lookup)

alert_receive_channels_ids = list(alert_receive_channels_qs.values_list("id", flat=True))
queryset = AlertGroup.objects.filter(channel__in=alert_receive_channels_ids)
print(alert_receive_channels_ids)


if team_values:
# need to also filter null team by null in in the receive channels so that we don't provide alertgroups where the team is assoicated to the channel
null_team_lookup = ((Q(teams__isnull=True) | Q(teams=None)) & Q(channel__in=alert_receive_channels_ids)) if NO_TEAM_VALUE in team_values else None
teams_lookup = (
Q(teams__public_primary_key__in=[ppk for ppk in team_values if ppk != NO_TEAM_VALUE]) | # handle alertgroups with teams property
((Q(teams__isnull=True) | Q(teams=None)) & Q(channel__in=alert_receive_channels_ids)) # handle alertgroups without a teams property set
)

if null_team_lookup:
teams_lookup = teams_lookup | null_team_lookup

# TODO also need to filter on integration as well.
if not ignore_filtering_by_available_teams:
queryset = AlertGroup.objects.filter(*self.available_teams_lookup_args)
else:
queryset = AlertGroup.objects

if team_values:
queryset = queryset.filter(teams_lookup)

if self.action in ("list", "stats") and not self.request.query_params.get("started_at"):
queryset = queryset.filter(started_at__gte=timezone.now() - timezone.timedelta(days=30))
Expand Down
Loading