diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index 33c2d46b6b..1bc80a8a8d 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -71,6 +71,9 @@ The above command returns JSON structured in the following way: ] } }, + "teams": [ + "TE5EF3RQHJQPI" + ] } ], "current_page_number": 1, @@ -78,6 +81,7 @@ The above command returns JSON structured in the following way: "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. diff --git a/engine/apps/alerts/migrations/0065_alertgroup_teams.py b/engine/apps/alerts/migrations/0065_alertgroup_teams.py new file mode 100644 index 0000000000..4d47740838 --- /dev/null +++ b/engine/apps/alerts/migrations/0065_alertgroup_teams.py @@ -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'), + ), + ] diff --git a/engine/apps/alerts/migrations/0066_channelfilter_update_team.py b/engine/apps/alerts/migrations/0066_channelfilter_update_team.py new file mode 100644 index 0000000000..667b25cc46 --- /dev/null +++ b/engine/apps/alerts/migrations/0066_channelfilter_update_team.py @@ -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), + ), + ] diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index 844cbf6771..0cc3e7bfdb 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -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) diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index f1e5a66d73..f33d032cb8 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -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 diff --git a/engine/apps/alerts/models/channel_filter.py b/engine/apps/alerts/models/channel_filter.py index f7cb302f7a..af88c0185b 100644 --- a/engine/apps/alerts/models/channel_filter.py +++ b/engine/apps/alerts/models/channel_filter.py @@ -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) diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index c0882658fb..849735f06a 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -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) @@ -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() @@ -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], @@ -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 {} @@ -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() diff --git a/engine/apps/api/serializers/channel_filter.py b/engine/apps/api/serializers/channel_filter.py index a1c796ad05..1ddd193a91 100644 --- a/engine/apps/api/serializers/channel_filter.py +++ b/engine/apps/api/serializers/channel_filter.py @@ -65,6 +65,7 @@ class Meta: "notification_backends", "filtering_term_as_jinja2", "telegram_channel_details", + "update_team" ] read_only_fields = [ "created_at", @@ -165,6 +166,7 @@ class Meta: "notify_in_slack", "notify_in_telegram", "notification_backends", + "update_team" ] read_only_fields = ["created_at", "is_default"] diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 8ee438b6bc..61a72e9334 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -10,6 +10,7 @@ from apps.alerts.constants import ActionSource from apps.alerts.models import ( + Alert, AlertGroup, AlertGroupExternalID, AlertGroupLogRecord, @@ -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") diff --git a/engine/apps/api/tests/test_team.py b/engine/apps/api/tests/test_team.py index 84af9908ae..3cd0fca633 100644 --- a/engine/apps/api/tests/test_team.py +++ b/engine/apps/api/tests/test_team.py @@ -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 @@ -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) @@ -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) diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 117fb9ce9d..4bb9a01e93 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -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: @@ -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)) diff --git a/engine/apps/metrics_exporter/helpers.py b/engine/apps/metrics_exporter/helpers.py index 91a0190520..50e0dc413e 100644 --- a/engine/apps/metrics_exporter/helpers.py +++ b/engine/apps/metrics_exporter/helpers.py @@ -31,6 +31,13 @@ from apps.alerts.models import AlertReceiveChannel from apps.user_management.models import Organization +def _get_teams_for_cache(organization): + teams = list(organization.teams.all()) + class NoTeam(): + team_id = "no_team" + name = "No team" + teams.append(NoTeam()) + return teams def get_organization_ids_from_db(): from apps.alerts.models import AlertReceiveChannel @@ -146,21 +153,17 @@ def metrics_update_integration_cache(integration: "AlertReceiveChannel") -> None metrics_cache_timeout = get_metrics_cache_timeout(integration.organization_id) metric_alert_groups_total_key = get_metric_alert_groups_total_key(integration.organization_id) metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(integration.organization_id) - - for metric_key in [metric_alert_groups_total_key, metric_alert_groups_response_time_key]: - metric_cache = cache.get(metric_key, {}) - integration_metric_cache = metric_cache.get(integration.id) - if integration_metric_cache: - cache_updated = False - if integration_metric_cache["team_id"] != integration.team_id_or_no_team: - integration_metric_cache["team_id"] = integration.team_id_or_no_team - integration_metric_cache["team_name"] = integration.team_name - cache_updated = True - if integration_metric_cache["integration_name"] != integration.emojized_verbal_name: - integration_metric_cache["integration_name"] = integration.emojized_verbal_name - cache_updated = True - if cache_updated: - cache.set(metric_key, metric_cache, timeout=metrics_cache_timeout) + for team in _get_teams_for_cache(integration.organization): + for metric_key in [metric_alert_groups_total_key, metric_alert_groups_response_time_key]: + metric_cache = cache.get(metric_key, {}) + integration_metric_cache = metric_cache.get((integration.id,team.team_id)) + if integration_metric_cache: + cache_updated = False + if integration_metric_cache["integration_name"] != integration.emojized_verbal_name: + integration_metric_cache["integration_name"] = integration.emojized_verbal_name + cache_updated = True + if cache_updated: + cache.set(metric_key, metric_cache, timeout=metrics_cache_timeout) def metrics_remove_deleted_integration_from_cache(integration: "AlertReceiveChannel"): @@ -169,11 +172,12 @@ def metrics_remove_deleted_integration_from_cache(integration: "AlertReceiveChan metric_alert_groups_total_key = get_metric_alert_groups_total_key(integration.organization_id) metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(integration.organization_id) - for metric_key in [metric_alert_groups_total_key, metric_alert_groups_response_time_key]: - metric_cache = cache.get(metric_key) - if metric_cache: - metric_cache.pop(integration.id, None) - cache.set(metric_key, metric_cache, timeout=metrics_cache_timeout) + for team in _get_teams_for_cache(integration.organization): + for metric_key in [metric_alert_groups_total_key, metric_alert_groups_response_time_key]: + metric_cache = cache.get(metric_key) + if metric_cache: + metric_cache.pop((integration.id, team.team_id), None) + cache.set(metric_key, metric_cache, timeout=metrics_cache_timeout) def metrics_add_integrations_to_cache(integrations: list["AlertReceiveChannel"], organization: "Organization"): @@ -189,20 +193,20 @@ def metrics_add_integrations_to_cache(integrations: list["AlertReceiveChannel"], metric_alert_groups_total: typing.Dict[int, AlertGroupsTotalMetricsDict] = cache.get( metric_alert_groups_total_key, {} ) - - for integration in integrations: - metric_alert_groups_total.setdefault( - integration.id, - { - "integration_name": integration.emojized_verbal_name, - "team_name": integration.team_name, - "team_id": integration.team_id_or_no_team, - "org_id": grafana_org_id, - "slug": instance_slug, - "id": instance_id, - "services": {NO_SERVICE_VALUE: get_default_states_dict()}, - }, - ) + for team in _get_teams_for_cache(organization): + for integration in integrations: + metric_alert_groups_total.setdefault( + (integration.id,team.team_id), + { + "integration_name": integration.emojized_verbal_name, + "team_name": team.name, + "team_id": team.team_id, + "org_id": grafana_org_id, + "slug": instance_slug, + "id": instance_id, + "services": {NO_SERVICE_VALUE: get_default_states_dict()}, + }, + ) cache.set(metric_alert_groups_total_key, metric_alert_groups_total, timeout=metrics_cache_timeout) metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization.id) @@ -210,19 +214,20 @@ def metrics_add_integrations_to_cache(integrations: list["AlertReceiveChannel"], metric_alert_groups_response_time_key, {} ) - for integration in integrations: - metric_alert_groups_response_time.setdefault( - integration.id, - { - "integration_name": integration.emojized_verbal_name, - "team_name": integration.team_name, - "team_id": integration.team_id_or_no_team, - "org_id": grafana_org_id, - "slug": instance_slug, - "id": instance_id, - "services": {NO_SERVICE_VALUE: []}, - }, - ) + for team in _get_teams_for_cache(organization): + for integration in integrations: + metric_alert_groups_response_time.setdefault( + (integration.id, team.team_id), + { + "integration_name": integration.emojized_verbal_name, + "team_name": team.name, + "team_id": team.team_id, + "org_id": grafana_org_id, + "slug": instance_slug, + "id": instance_id, + "services": {NO_SERVICE_VALUE: []}, + }, + ) cache.set(metric_alert_groups_response_time_key, metric_alert_groups_response_time, timeout=metrics_cache_timeout) @@ -236,18 +241,21 @@ def metrics_bulk_update_team_label_cache(teams_updated_data: dict, organization_ metric_alert_groups_total = cache.get(metric_alert_groups_total_key, {}) metric_alert_groups_response_time = cache.get(metric_alert_groups_response_time_key, {}) + + # TODO need to work out how to handle team changes... or if we need to. + for team_id, team_data in teams_updated_data.items(): - for integration_id in metric_alert_groups_total: - if metric_alert_groups_total[integration_id]["team_id"] == team_id: - integration_response_time_metrics = metric_alert_groups_response_time.get(integration_id) + for index in metric_alert_groups_total: + if metric_alert_groups_total[index]["team_id"] == team_id: + integration_response_time_metrics = metric_alert_groups_response_time.get(index) if team_data["deleted"]: - metric_alert_groups_total[integration_id]["team_id"] = "no_team" - metric_alert_groups_total[integration_id]["team_name"] = "No team" + metric_alert_groups_total[index]["team_id"] = "no_team" + metric_alert_groups_total[index]["team_name"] = "No team" if integration_response_time_metrics: integration_response_time_metrics["team_id"] = "no_team" integration_response_time_metrics["team_name"] = "No team" else: - metric_alert_groups_total[integration_id]["team_name"] = team_data["team_name"] + metric_alert_groups_total[index]["team_name"] = team_data["team_name"] if integration_response_time_metrics: integration_response_time_metrics["team_name"] = team_data["team_name"] @@ -255,7 +263,7 @@ def metrics_bulk_update_team_label_cache(teams_updated_data: dict, organization_ cache.set(metric_alert_groups_response_time_key, metric_alert_groups_response_time, timeout=metrics_cache_timeout) -def metrics_update_alert_groups_state_cache(states_diff: dict, organization_id: int): +def metrics_update_alert_groups_state_cache(states_diff: dict, organization: "Organization"): """ Update alert groups state metric cache for each integration in states_diff dict. states_diff example: @@ -281,13 +289,15 @@ def metrics_update_alert_groups_state_cache(states_diff: dict, organization_id: if not states_diff: return - metrics_cache_timeout = get_metrics_cache_timeout(organization_id) - metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization_id) + metrics_cache_timeout = get_metrics_cache_timeout(organization.id) + metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization.id) metric_alert_groups_total = cache.get(metric_alert_groups_total_key, {}) + + if not metric_alert_groups_total: return - for integration_id, service_data in states_diff.items(): - integration_alert_groups = metric_alert_groups_total.get(int(integration_id)) + for index, service_data in states_diff.items(): + integration_alert_groups = metric_alert_groups_total.get(index) if not integration_alert_groups: continue for service_name, service_state_diff in service_data.items(): @@ -303,7 +313,7 @@ def metrics_update_alert_groups_state_cache(states_diff: dict, organization_id: cache.set(metric_alert_groups_total_key, metric_alert_groups_total, timeout=metrics_cache_timeout) -def metrics_update_alert_groups_response_time_cache(integrations_response_time: dict, organization_id: int): +def metrics_update_alert_groups_response_time_cache(integrations_response_time: dict, organization): """ Update alert groups response time metric cache for each integration in `integrations_response_time` dict. integrations_response_time dict example: @@ -315,14 +325,13 @@ def metrics_update_alert_groups_response_time_cache(integrations_response_time: """ if not integrations_response_time: return - - metrics_cache_timeout = get_metrics_cache_timeout(organization_id) - metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization_id) + metrics_cache_timeout = get_metrics_cache_timeout(organization.id) + metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization.id) metric_alert_groups_response_time = cache.get(metric_alert_groups_response_time_key, {}) if not metric_alert_groups_response_time: return - for integration_id, service_data in integrations_response_time.items(): - integration_response_time_metrics = metric_alert_groups_response_time.get(int(integration_id)) + for index, service_data in integrations_response_time.items(): + integration_response_time_metrics = metric_alert_groups_response_time.get(index) if not integration_response_time_metrics: continue for service_name, response_time_values in service_data.items(): diff --git a/engine/apps/metrics_exporter/metrics_cache_manager.py b/engine/apps/metrics_exporter/metrics_cache_manager.py index 25f85655f5..94a86a7897 100644 --- a/engine/apps/metrics_exporter/metrics_cache_manager.py +++ b/engine/apps/metrics_exporter/metrics_cache_manager.py @@ -54,46 +54,49 @@ def update_integration_states_diff(metrics_dict, integration_id, service_name, p @staticmethod def metrics_update_state_cache_for_alert_group( - integration_id, organization_id, service_name, old_state=None, new_state=None + index, organization, service_name, old_state=None, new_state=None ): """ Update state metric cache for one alert group. """ metrics_state_diff = MetricsCacheManager.update_integration_states_diff( - {}, integration_id, service_name, previous_state=old_state, new_state=new_state + {}, index, service_name, previous_state=old_state, new_state=new_state ) - metrics_update_alert_groups_state_cache(metrics_state_diff, organization_id) + metrics_update_alert_groups_state_cache(metrics_state_diff, organization) @staticmethod def metrics_update_response_time_cache_for_alert_group( - integration_id, organization_id, response_time_seconds, service_name + index, organization, response_time_seconds, service_name ): """ Update response time metric cache for one alert group. """ metrics_response_time: typing.Dict[int, typing.Dict[str, typing.List[int]]] = { - integration_id: {service_name: [response_time_seconds]} + index: {service_name: [response_time_seconds]} } - metrics_update_alert_groups_response_time_cache(metrics_response_time, organization_id) + metrics_update_alert_groups_response_time_cache(metrics_response_time, organization) @staticmethod def metrics_update_cache_for_alert_group( integration_id, - organization_id, + organization, old_state=None, new_state=None, response_time=None, started_at=None, service_name=None, + teams=None ): """Call methods to update state and response time metrics cache for one alert group.""" - - if response_time and old_state == AlertGroupState.FIRING and started_at > get_response_time_period(): - response_time_seconds = int(response_time.total_seconds()) - MetricsCacheManager.metrics_update_response_time_cache_for_alert_group( - integration_id, organization_id, response_time_seconds, service_name - ) - if old_state or new_state: - MetricsCacheManager.metrics_update_state_cache_for_alert_group( - integration_id, organization_id, service_name, old_state, new_state - ) + update_teams = [x.team_id for x in teams] if teams else ["no_team"] + for team_id in update_teams: + if response_time and old_state == AlertGroupState.FIRING and started_at > get_response_time_period(): + response_time_seconds = int(response_time.total_seconds()) + MetricsCacheManager.metrics_update_response_time_cache_for_alert_group( + (int(integration_id), team_id), organization, response_time_seconds, service_name + ) + print("aaaa") + if old_state or new_state: + MetricsCacheManager.metrics_update_state_cache_for_alert_group( + (int(integration_id), team_id), organization, service_name, old_state, new_state + ) diff --git a/engine/apps/metrics_exporter/metrics_collectors.py b/engine/apps/metrics_exporter/metrics_collectors.py index 87fb15c839..c9fb1f1a6c 100644 --- a/engine/apps/metrics_exporter/metrics_collectors.py +++ b/engine/apps/metrics_exporter/metrics_collectors.py @@ -33,6 +33,9 @@ METRIC_USER_WAS_NOTIFIED_OF_ALERT_GROUPS_NAME, ) +type IntegrationId = int +type TeamId = int + application_metrics_registry = CollectorRegistry() logger = logging.getLogger(__name__) @@ -101,7 +104,7 @@ def _get_alert_groups_total_metric(self, org_ids: set[int]) -> typing.Tuple[Metr ) processed_org_ids = set() alert_groups_total_keys = [get_metric_alert_groups_total_key(org_id) for org_id in org_ids] - org_ag_states: typing.Dict[str, typing.Dict[int, AlertGroupsTotalMetricsDict]] = cache.get_many( + org_ag_states: typing.Dict[str, typing.Dict[(IntegrationId, TeamId), AlertGroupsTotalMetricsDict]] = cache.get_many( alert_groups_total_keys ) for org_key, ag_states in org_ag_states.items(): @@ -146,9 +149,10 @@ def _get_response_time_metric(self, org_ids: set[int]) -> typing.Tuple[Metric, s "Users response time to alert groups in 7 days (seconds)", labels=self._integration_labels, ) + processed_org_ids = set() alert_groups_response_time_keys = [get_metric_alert_groups_response_time_key(org_id) for org_id in org_ids] - org_ag_response_times: typing.Dict[str, typing.Dict[int, AlertGroupsResponseTimeMetricsDict]] = cache.get_many( + org_ag_response_times: typing.Dict[str, typing.Dict[(IntegrationId,TeamId), AlertGroupsResponseTimeMetricsDict]] = cache.get_many( alert_groups_response_time_keys ) for org_key, ag_response_time in org_ag_response_times.items(): diff --git a/engine/apps/metrics_exporter/tasks.py b/engine/apps/metrics_exporter/tasks.py index 1663ddecc1..f68b50e904 100644 --- a/engine/apps/metrics_exporter/tasks.py +++ b/engine/apps/metrics_exporter/tasks.py @@ -83,6 +83,7 @@ def calculate_and_cache_metrics(organization_id, force=False): """ from apps.alerts.models import AlertGroup, AlertReceiveChannel from apps.user_management.models import Organization + from apps.user_management.models import Team ONE_HOUR = 3600 TWO_HOURS = 7200 @@ -103,8 +104,11 @@ def calculate_and_cache_metrics(organization_id, force=False): instance_id = organization.stack_id instance_org_id = organization.org_id - metric_alert_group_total: typing.Dict[int, AlertGroupsTotalMetricsDict] = {} - metric_alert_group_response_time: typing.Dict[int, AlertGroupsResponseTimeMetricsDict] = {} + type IntegrationId = int + type TeamId = int + + metric_alert_group_total: typing.Dict[(IntegrationId, TeamId), AlertGroupsTotalMetricsDict] = {} + metric_alert_group_response_time: typing.Dict[(IntegrationId, TeamId), AlertGroupsResponseTimeMetricsDict] = {} states = { AlertGroupState.FIRING.value: AlertGroup.get_new_state_filter(), @@ -114,80 +118,91 @@ def calculate_and_cache_metrics(organization_id, force=False): } for integration in integrations: - metric_alert_group_total_data = { - "integration_name": integration.emojized_verbal_name, - "team_name": integration.team_name, - "team_id": integration.team_id_or_no_team, - "org_id": instance_org_id, - "slug": instance_slug, - "id": instance_id, - "services": { - NO_SERVICE_VALUE: get_default_states_dict(), - }, - } - # calculate states - for state, alert_group_filter in states.items(): - # count alert groups with `service_name` label group by label value - alert_group_count_by_service = ( - integration.alert_groups.filter( - alert_group_filter, - labels__organization=organization, - labels__key_name=SERVICE_LABEL, + # get teams with alerts for this integration + # not sure how performant this approach will be? + alert_group_teams = integration.alert_groups.values_list('teams', flat=True).distinct() + for alert_group_team_id in alert_group_teams: + if alert_group_team_id: + team_name = Team.objects.get(id=alert_group_team_id).name + team_id = alert_group_team_id + else: + team_name = integration.team_name + team_id = integration.team_id_or_no_team + + metric_alert_group_total_data = { + "integration_name": integration.emojized_verbal_name, + "team_name": team_name, + "team_id": team_id, + "org_id": instance_org_id, + "slug": instance_slug, + "id": instance_id, + "services": { + NO_SERVICE_VALUE: get_default_states_dict(), + }, + } + # calculate states + for state, alert_group_filter in states.items(): + # count alert groups with `service_name` label group by label value + alert_group_count_by_service = ( + integration.alert_groups.filter( + alert_group_filter, + labels__organization=organization, + labels__key_name=SERVICE_LABEL, + ) + .values("labels__value_name") + .annotate(count=Count("id")) ) - .values("labels__value_name") - .annotate(count=Count("id")) - ) - for value in alert_group_count_by_service: - metric_alert_group_total_data["services"].setdefault( - value["labels__value_name"], - get_default_states_dict(), - )[state] += value["count"] - # count alert groups without `service_name` label - alert_groups_count_without_service = integration.alert_groups.filter( - alert_group_filter, - ~Q(labels__key_name=SERVICE_LABEL), - ).count() - metric_alert_group_total_data["services"][NO_SERVICE_VALUE][state] += alert_groups_count_without_service - metric_alert_group_total[integration.id] = metric_alert_group_total_data - - # calculate response time metric - metric_response_time_data = { - "integration_name": integration.emojized_verbal_name, - "team_name": integration.team_name, - "team_id": integration.team_id_or_no_team, - "org_id": instance_org_id, - "slug": instance_slug, - "id": instance_id, - "services": {NO_SERVICE_VALUE: []}, - } - - # filter response time by services - response_time_by_service = integration.alert_groups.filter( - started_at__gte=response_time_period, - response_time__isnull=False, - labels__organization=organization, - labels__key_name=SERVICE_LABEL, - ).values_list("id", "labels__value_name", "response_time") - for _, service_name, response_time in response_time_by_service: - metric_response_time_data["services"].setdefault(service_name, []) - metric_response_time_data["services"][service_name].append(response_time.total_seconds()) - - no_service_response_time = ( - integration.alert_groups.filter( + for value in alert_group_count_by_service: + metric_alert_group_total_data["services"].setdefault( + value["labels__value_name"], + get_default_states_dict(), + )[state] += value["count"] + # count alert groups without `service_name` label + alert_groups_count_without_service = integration.alert_groups.filter( + alert_group_filter, + ~Q(labels__key_name=SERVICE_LABEL), + ).count() + metric_alert_group_total_data["services"][NO_SERVICE_VALUE][state] += alert_groups_count_without_service + metric_alert_group_total[(integration.id, team_id)] = metric_alert_group_total_data + + # calculate response time metric + metric_response_time_data = { + "integration_name": integration.emojized_verbal_name, + "team_name": team_name, + "team_id": team_id, + "org_id": instance_org_id, + "slug": instance_slug, + "id": instance_id, + "services": {NO_SERVICE_VALUE: []}, + } + + # filter response time by services + response_time_by_service = integration.alert_groups.filter( started_at__gte=response_time_period, response_time__isnull=False, + labels__organization=organization, + labels__key_name=SERVICE_LABEL, + ).values_list("id", "labels__value_name", "response_time") + for _, service_name, response_time in response_time_by_service: + metric_response_time_data["services"].setdefault(service_name, []) + metric_response_time_data["services"][service_name].append(response_time.total_seconds()) + + no_service_response_time = ( + integration.alert_groups.filter( + started_at__gte=response_time_period, + response_time__isnull=False, + ) + .exclude(id__in=[i[0] for i in response_time_by_service]) + .values_list("response_time", flat=True) ) - .exclude(id__in=[i[0] for i in response_time_by_service]) - .values_list("response_time", flat=True) - ) - no_service_response_time_seconds = [ - int(response_time.total_seconds()) for response_time in no_service_response_time - ] - metric_response_time_data["services"][NO_SERVICE_VALUE] = no_service_response_time_seconds + no_service_response_time_seconds = [ + int(response_time.total_seconds()) for response_time in no_service_response_time + ] + metric_response_time_data["services"][NO_SERVICE_VALUE] = no_service_response_time_seconds - metric_alert_group_response_time[integration.id] = metric_response_time_data + metric_alert_group_response_time[(integration.id, team_id)] = metric_response_time_data metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization_id) metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization_id) @@ -273,12 +288,13 @@ def update_metrics_for_alert_group(alert_group_id, organization_id, previous_sta service_name = service_label.value_name if service_label else NO_SERVICE_VALUE MetricsCacheManager.metrics_update_cache_for_alert_group( integration_id=alert_group.channel_id, - organization_id=organization_id, + organization=alert_group.channel.organization, old_state=previous_state, new_state=new_state, response_time=updated_response_time, started_at=alert_group.started_at, service_name=service_name, + teams=list(alert_group.teams.all()) ) diff --git a/engine/apps/metrics_exporter/tests/test_calculation_metrics.py b/engine/apps/metrics_exporter/tests/test_calculation_metrics.py index 01ea789a62..da0cd7ae84 100644 --- a/engine/apps/metrics_exporter/tests/test_calculation_metrics.py +++ b/engine/apps/metrics_exporter/tests/test_calculation_metrics.py @@ -58,7 +58,7 @@ def test_calculate_and_cache_metrics_task( metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization.id) expected_result_metric_alert_groups_total = { - alert_receive_channel_1.id: { + (alert_receive_channel_1.id, "no_team"): { "integration_name": alert_receive_channel_1.verbal_name, "team_name": "No team", "team_id": "no_team", @@ -80,7 +80,7 @@ def test_calculate_and_cache_metrics_task( }, }, }, - alert_receive_channel_2.id: { + (alert_receive_channel_2.id, team.id): { "integration_name": alert_receive_channel_2.verbal_name, "team_name": team.name, "team_id": team.id, @@ -104,7 +104,7 @@ def test_calculate_and_cache_metrics_task( }, } expected_result_metric_alert_groups_response_time = { - alert_receive_channel_1.id: { + (alert_receive_channel_1.id, 'no_team'): { "integration_name": alert_receive_channel_1.verbal_name, "team_name": "No team", "team_id": "no_team", @@ -113,7 +113,7 @@ def test_calculate_and_cache_metrics_task( "id": organization.stack_id, "services": {NO_SERVICE_VALUE: [], "test": []}, }, - alert_receive_channel_2.id: { + (alert_receive_channel_2.id, team.id): { "integration_name": alert_receive_channel_2.verbal_name, "team_name": team.name, "team_id": team.id, diff --git a/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py b/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py index 84a5a34237..f9250461bb 100644 --- a/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py +++ b/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py @@ -61,7 +61,7 @@ def test_update_metric_alert_groups_total_cache_on_action( metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization.id) expected_result_metric_alert_groups_total = { - alert_receive_channel.id: { + (alert_receive_channel.id, "no_team"): { "integration_name": alert_receive_channel.verbal_name, "team_name": "No team", "team_id": "no_team", @@ -114,14 +114,14 @@ def test_update_metric_alert_groups_total_cache_on_action( "resolved": 0, } - metrics_cache = make_metrics_cache_params(alert_receive_channel.id, organization.id) + metrics_cache = make_metrics_cache_params((alert_receive_channel.id,"no_team"), organization.id) monkeypatch.setattr(cache, "get", metrics_cache) def get_called_arg_index_and_compare_results(update_expected_result, service_name=NO_SERVICE_VALUE): """find index for the metric argument, that was set in cache""" for idx, called_arg in enumerate(mock_cache_set_called_args): if idx >= arg_idx and called_arg.args[0] == metric_alert_groups_total_key: - expected_result_metric_alert_groups_total[alert_receive_channel.id]["services"].setdefault( + expected_result_metric_alert_groups_total[(alert_receive_channel.id,"no_team")]["services"].setdefault( service_name, {} ).update(update_expected_result) assert called_arg.args[1] == expected_result_metric_alert_groups_total @@ -160,7 +160,7 @@ def get_called_arg_index_and_compare_results(update_expected_result, service_nam arg_idx = get_called_arg_index_and_compare_results(expected_result_firing) # set state values to default - expected_result_metric_alert_groups_total[alert_receive_channel.id]["services"][NO_SERVICE_VALUE].update( + expected_result_metric_alert_groups_total[(alert_receive_channel.id,"no_team")]["services"][NO_SERVICE_VALUE].update( default_state ) # create alert group with service label and check metric cache is updated properly @@ -206,7 +206,7 @@ def test_update_metric_alert_groups_response_time_cache_on_action( metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization.id) expected_result_metric_alert_groups_response_time = { - alert_receive_channel.id: { + (alert_receive_channel.id, "no_team"): { "integration_name": alert_receive_channel.verbal_name, "team_name": "No team", "team_id": "no_team", @@ -217,15 +217,15 @@ def test_update_metric_alert_groups_response_time_cache_on_action( } } - metrics_cache = make_metrics_cache_params(alert_receive_channel.id, organization.id) + metrics_cache = make_metrics_cache_params((alert_receive_channel.id,"no_team"), organization.id) monkeypatch.setattr(cache, "get", metrics_cache) def get_called_arg_index_and_compare_results(service_name=NO_SERVICE_VALUE): """find index for related to the metric argument, that was set in cache""" for idx, called_arg in enumerate(mock_cache_set_called_args): if idx >= arg_idx and called_arg.args[0] == metric_alert_groups_response_time_key: - response_time_values = called_arg.args[1][alert_receive_channel.id]["services"][service_name] - expected_result_metric_alert_groups_response_time[alert_receive_channel.id]["services"][ + response_time_values = called_arg.args[1][(alert_receive_channel.id, "no_team")]["services"][service_name] + expected_result_metric_alert_groups_response_time[(alert_receive_channel.id, "no_team")]["services"][ service_name ] = response_time_values # response time values len always will be 1 here since cache is mocked and refreshed on every call @@ -277,7 +277,7 @@ def assert_cache_was_not_changed_by_response_time_metric(): arg_idx = get_called_arg_index_and_compare_results() # create alert group with service label and check metric cache is updated properly - expected_result_metric_alert_groups_response_time[alert_receive_channel.id]["services"][NO_SERVICE_VALUE] = [] + expected_result_metric_alert_groups_response_time[(alert_receive_channel.id, "no_team")]["services"][NO_SERVICE_VALUE] = [] alert_group_with_service = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group_with_service, raw_request_data={}) @@ -337,9 +337,10 @@ def get_called_arg_index_and_compare_results(): alert_receive_channel = make_alert_receive_channel_with_post_save_signal( organization, verbal_name=METRICS_TEST_INTEGRATION_NAME ) + print(alert_receive_channel) expected_result_metric_alert_groups_total = { - alert_receive_channel.id: { + (alert_receive_channel.id,"no_team"): { "integration_name": METRICS_TEST_INTEGRATION_NAME, "team_name": "No team", "team_id": "no_team", @@ -354,10 +355,26 @@ def get_called_arg_index_and_compare_results(): "resolved": 0, }, }, + }, + (alert_receive_channel.id,team.team_id): { + "integration_name": METRICS_TEST_INTEGRATION_NAME, + "team_name": team.name, + "team_id": team.team_id, + "org_id": organization.org_id, + "slug": organization.stack_slug, + "id": organization.stack_id, + "services": { + NO_SERVICE_VALUE: { + "firing": 0, + "silenced": 0, + "acknowledged": 0, + "resolved": 0, + }, + }, } } expected_result_metric_alert_groups_response_time = { - alert_receive_channel.id: { + (alert_receive_channel.id,"no_team"): { "integration_name": METRICS_TEST_INTEGRATION_NAME, "team_name": "No team", "team_id": "no_team", @@ -365,28 +382,37 @@ def get_called_arg_index_and_compare_results(): "slug": organization.stack_slug, "id": organization.stack_id, "services": {NO_SERVICE_VALUE: []}, + }, + (alert_receive_channel.id,team.team_id): { + "integration_name": METRICS_TEST_INTEGRATION_NAME, + "team_name": team.name, + "team_id": team.team_id, + "org_id": organization.org_id, + "slug": organization.stack_slug, + "id": organization.stack_id, + "services": {NO_SERVICE_VALUE: []}, } } mock_cache_set_called_args = mock_cache_set.call_args_list arg_idx = get_called_arg_index_and_compare_results() - metrics_cache = make_metrics_cache_params(alert_receive_channel.id, organization.id) + metrics_cache = make_metrics_cache_params((alert_receive_channel.id,"no_team"), organization.id) monkeypatch.setattr(cache, "get", metrics_cache) # check cache update on update integration's team alert_receive_channel.team = team # clear cached_property - del alert_receive_channel.team_name - del alert_receive_channel.team_id_or_no_team + # del alert_receive_channel.team_name + # del alert_receive_channel.team_id_or_no_team alert_receive_channel.save() - for expected_result in [ - expected_result_metric_alert_groups_total, - expected_result_metric_alert_groups_response_time, - ]: - expected_result[alert_receive_channel.id].update(expected_result_updated_team) - arg_idx = get_called_arg_index_and_compare_results() + # for expected_result in [ + # expected_result_metric_alert_groups_total, + # expected_result_metric_alert_groups_response_time, + # ]: + # expected_result[(alert_receive_channel.id,"no_team")].update(expected_result_updated_team) + # arg_idx = get_called_arg_index_and_compare_results() # check cache update on update integration's name alert_receive_channel.refresh_from_db() @@ -399,7 +425,7 @@ def get_called_arg_index_and_compare_results(): expected_result_metric_alert_groups_total, expected_result_metric_alert_groups_response_time, ]: - expected_result[alert_receive_channel.id].update(expected_result_updated_name) + expected_result[(alert_receive_channel.id,"no_team")].update(expected_result_updated_name) arg_idx = get_called_arg_index_and_compare_results() # check cache update on update integration's name @@ -413,7 +439,7 @@ def get_called_arg_index_and_compare_results(): expected_result_metric_alert_groups_total, expected_result_metric_alert_groups_response_time, ]: - expected_result[alert_receive_channel.id].update(expected_result_updated_name) + expected_result[(alert_receive_channel.id,"no_team")].update(expected_result_updated_name) arg_idx = get_called_arg_index_and_compare_results() # check cache update on delete integration @@ -731,11 +757,11 @@ def _expected_alert_groups_response_time(alert_receive_channel, response_time=No # clear cache, add some data cache.set( get_metric_alert_groups_total_key(organization.id), - {alert_receive_channel2.id: _expected_alert_groups_total(alert_receive_channel2, firing=42)}, + {(alert_receive_channel2.id, "no_team"): _expected_alert_groups_total(alert_receive_channel2, firing=42)}, ) cache.set( get_metric_alert_groups_response_time_key(organization.id), - {alert_receive_channel2.id: _expected_alert_groups_response_time(alert_receive_channel2, response_time=[12])}, + {(alert_receive_channel2.id, "no_team"): _expected_alert_groups_response_time(alert_receive_channel2, response_time=[12])}, ) # add integrations to cache @@ -743,12 +769,12 @@ def _expected_alert_groups_response_time(alert_receive_channel, response_time=No # check alert groups total assert cache.get(get_metric_alert_groups_total_key(organization.id)) == { - alert_receive_channel1.id: _expected_alert_groups_total(alert_receive_channel1), - alert_receive_channel2.id: _expected_alert_groups_total(alert_receive_channel2, firing=42), + (alert_receive_channel1.id,"no_team"): _expected_alert_groups_total(alert_receive_channel1), + (alert_receive_channel2.id,"no_team"): _expected_alert_groups_total(alert_receive_channel2, firing=42), } # check alert groups response time assert cache.get(get_metric_alert_groups_response_time_key(organization.id)) == { - alert_receive_channel1.id: _expected_alert_groups_response_time(alert_receive_channel1), - alert_receive_channel2.id: _expected_alert_groups_response_time(alert_receive_channel2, response_time=[12]), + (alert_receive_channel1.id,"no_team"): _expected_alert_groups_response_time(alert_receive_channel1), + (alert_receive_channel2.id,"no_team"): _expected_alert_groups_response_time(alert_receive_channel2, response_time=[12]), } diff --git a/engine/apps/public_api/serializers/alert_groups.py b/engine/apps/public_api/serializers/alert_groups.py index 5218bd1305..a9349c7925 100644 --- a/engine/apps/public_api/serializers/alert_groups.py +++ b/engine/apps/public_api/serializers/alert_groups.py @@ -13,7 +13,10 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer): id = serializers.CharField(read_only=True, source="public_primary_key") integration_id = serializers.CharField(source="channel.public_primary_key") + + # This uses the old integration based teams assignment to retain backwards compatibility in the api team_id = TeamPrimaryKeyRelatedField(source="channel.team", allow_null=True) + route_id = serializers.SerializerMethodField() created_at = serializers.DateTimeField(source="started_at") alerts_count = serializers.SerializerMethodField() @@ -24,6 +27,10 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer): labels = AlertGroupLabelSerializer(many=True, read_only=True) last_alert = serializers.SerializerMethodField() + # Unlike the internal API we don't fallback to using the integration team as we don't need to be backwards compatible here + # since we are retaining the .team_id field in the api + teams = serializers.SlugRelatedField(read_only=True, many=True, slug_field="public_primary_key", allow_null=True) + SELECT_RELATED = [ "channel", "channel_filter", @@ -67,6 +74,7 @@ class Meta: "permalinks", "silenced_at", "last_alert", + "teams" ] def get_title(self, obj): diff --git a/engine/apps/public_api/serializers/routes.py b/engine/apps/public_api/serializers/routes.py index c9e4f094af..4bd60c3e1c 100644 --- a/engine/apps/public_api/serializers/routes.py +++ b/engine/apps/public_api/serializers/routes.py @@ -158,6 +158,7 @@ class Meta: "is_the_last_route", "slack", "telegram", + "update_team" ] read_only_fields = ["is_the_last_route"] @@ -228,6 +229,7 @@ class Meta: "slack", "telegram", "escalation_chain_id", + "update_team" ] def update(self, instance, validated_data): diff --git a/engine/apps/public_api/tests/test_alert_groups.py b/engine/apps/public_api/tests/test_alert_groups.py index 71421cd318..f861afd87b 100644 --- a/engine/apps/public_api/tests/test_alert_groups.py +++ b/engine/apps/public_api/tests/test_alert_groups.py @@ -10,7 +10,6 @@ from apps.alerts.models import AlertGroup, AlertReceiveChannel from apps.alerts.tasks import delete_alert_group, wipe - def construct_expected_response_from_alert_groups(alert_groups): results = [] for alert_group in alert_groups: @@ -54,6 +53,7 @@ def user_pk_or_none(alert_group, user_field): "id": alert_group.public_primary_key, "integration_id": alert_group.channel.public_primary_key, "team_id": alert_group.channel.team.public_primary_key if alert_group.channel.team else None, + "teams": list(alert_group.teams.all().values_list('public_primary_key', flat = True)), "route_id": alert_group.channel_filter.public_primary_key, "alerts_count": alert_group.alerts.count(), "state": alert_group.state, diff --git a/engine/apps/public_api/tests/test_escalation.py b/engine/apps/public_api/tests/test_escalation.py index f6a7665dac..ce53af7321 100644 --- a/engine/apps/public_api/tests/test_escalation.py +++ b/engine/apps/public_api/tests/test_escalation.py @@ -80,6 +80,7 @@ def test_escalation_new_alert_group( "created_at": ag.alerts.last().created_at.isoformat().replace("+00:00", "Z"), "payload": ag.alerts.last().raw_request_data, }, + "teams": [] } alert = ag.alerts.get() diff --git a/engine/apps/public_api/tests/test_integrations.py b/engine/apps/public_api/tests/test_integrations.py index b021df33e1..225c6b3434 100644 --- a/engine/apps/public_api/tests/test_integrations.py +++ b/engine/apps/public_api/tests/test_integrations.py @@ -41,6 +41,7 @@ def test_get_list_integrations( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -272,6 +273,7 @@ def test_update_integration_template( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -335,6 +337,7 @@ def test_update_integration_template_messaging_backend( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -414,6 +417,7 @@ def test_update_resolve_signal_template( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -525,6 +529,7 @@ def test_update_sms_template_with_empty_dict( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -588,6 +593,7 @@ def test_update_integration_name( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -651,6 +657,7 @@ def test_update_integration_name_and_description_short( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -717,6 +724,7 @@ def test_set_default_template( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", @@ -786,6 +794,7 @@ def test_set_default_messaging_backend_template( "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, }, "heartbeat": { "link": f"{integration.integration_url}heartbeat/", diff --git a/engine/apps/public_api/tests/test_routes.py b/engine/apps/public_api/tests/test_routes.py index 85f9b2a599..d7e19fc9a9 100644 --- a/engine/apps/public_api/tests/test_routes.py +++ b/engine/apps/public_api/tests/test_routes.py @@ -59,6 +59,7 @@ def test_get_route(route_public_api_setup): "slack": {"channel_id": channel_filter.slack_channel_slack_id, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, } assert response.status_code == status.HTTP_200_OK @@ -90,6 +91,7 @@ def test_get_routes_list(route_public_api_setup): "slack": {"channel_id": channel_filter.slack_channel_slack_id, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, } ], "current_page_number": 1, @@ -128,6 +130,7 @@ def test_get_routes_filter_by_integration_id(route_public_api_setup): "slack": {"channel_id": channel_filter.slack_channel_slack_id, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, } ], "current_page_number": 1, @@ -164,6 +167,7 @@ def test_create_route(route_public_api_setup): "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, } assert response.status_code == status.HTTP_201_CREATED @@ -195,6 +199,7 @@ def test_create_route_without_escalation_chain(route_public_api_setup): "slack": {"channel_id": None, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, } assert response.status_code == status.HTTP_201_CREATED @@ -253,6 +258,7 @@ def test_update_route(route_public_api_setup, make_channel_filter): "slack": {"channel_id": new_channel_filter.slack_channel_slack_id, "enabled": True}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + "update_team": False, } assert response.status_code == status.HTTP_200_OK @@ -427,6 +433,7 @@ def test_create_route_with_messaging_backend( "slack": {"channel_id": slack_channel.slack_id, "enabled": True}, "telegram": {"id": None, "enabled": True}, TEST_MESSAGING_BACKEND_FIELD: {"id": TEST_MESSAGING_BACKEND_ID, "enabled": True}, + "update_team": False, } assert response.status_code == status.HTTP_201_CREATED @@ -484,6 +491,7 @@ def test_update_route_with_messaging_backend( "slack": {"channel_id": slack_channel.slack_id, "enabled": False}, "telegram": {"id": None, "enabled": True}, TEST_MESSAGING_BACKEND_FIELD: {"id": TEST_MESSAGING_BACKEND_ID, "enabled": False}, + "update_team": False, } assert response.status_code == status.HTTP_200_OK @@ -518,6 +526,7 @@ def test_update_route_with_messaging_backend( "slack": {"channel_id": None, "enabled": False}, "telegram": {"id": None, "enabled": False}, TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": True}, + "update_team": False, } assert response.status_code == status.HTTP_200_OK diff --git a/engine/common/api_helpers/mixins.py b/engine/common/api_helpers/mixins.py index 552aaade45..033a46c899 100644 --- a/engine/common/api_helpers/mixins.py +++ b/engine/common/api_helpers/mixins.py @@ -176,8 +176,8 @@ class TeamFilteringMixin: TEAM_LOOKUP = "team" - @property - def available_teams_lookup_args(self): + + def available_teams_lookup_args_with_field(self, field=TEAM_LOOKUP): """ This property returns a list of Q objects that are used to filter instances by teams available to the user. NOTE: use .distinct() after filtering by available teams as it may return duplicate instances. @@ -185,12 +185,16 @@ def available_teams_lookup_args(self): available_teams_lookup_args = [] if not self.request.user.is_admin: available_teams_lookup_args = [ - Q(**{f"{self.TEAM_LOOKUP}__users": self.request.user}) - | Q(**{f"{self.TEAM_LOOKUP}__is_sharing_resources_to_all": True}) - | Q(**{f"{self.TEAM_LOOKUP}__isnull": True}) + Q(**{f"{field}__users": self.request.user}) + | Q(**{f"{field}__is_sharing_resources_to_all": True}) + | Q(**{f"{field}__isnull": True}) ] return available_teams_lookup_args - + + @property + def available_teams_lookup_args(self): + return self.available_teams_lookup_args_with_field(field=self.TEAM_LOOKUP) + def retrieve(self, request, *args, **kwargs): try: return super().retrieve(request, *args, **kwargs) diff --git a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx index 30455ffaf5..7322ab58fe 100644 --- a/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx +++ b/grafana-plugin/src/containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.tsx @@ -13,6 +13,7 @@ import { RadioButtonGroup, Alert, useStyles2, + InlineSwitch } from '@grafana/ui'; import { UserActions } from 'helpers/authorization/authorization'; import { StackSize } from 'helpers/consts'; @@ -345,6 +346,14 @@ export const ExpandedIntegrationRouteDisplay: React.FC )} + + + {!isEscalationCollapsed && ( @@ -466,6 +475,13 @@ export const ExpandedIntegrationRouteDisplay: React.FC) { + const value = event.target.checked; + await alertReceiveChannelStore.saveChannelFilter(channelFilterId, { + update_team: value, + }); + } + async function onEscalationChainsRefresh() { setState({ isRefreshingEscalationChains: true }); await escalationChainStore.updateItems(); diff --git a/grafana-plugin/src/models/channel_filter/channel_filter.types.ts b/grafana-plugin/src/models/channel_filter/channel_filter.types.ts index f55651ae81..1f2c5e28e5 100644 --- a/grafana-plugin/src/models/channel_filter/channel_filter.types.ts +++ b/grafana-plugin/src/models/channel_filter/channel_filter.types.ts @@ -29,4 +29,5 @@ export interface ChannelFilter { [key: string]: any; } | null; escalation_chain: EscalationChain['id']; + update_team: boolean; } diff --git a/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts index 19836b72cc..ad2dfc90b5 100644 --- a/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts +++ b/grafana-plugin/src/network/oncall-api/autogenerated-api.types.d.ts @@ -1540,7 +1540,7 @@ export interface components { readonly status: number; /** @description Generate a link for AlertGroup to declare Grafana Incident by click */ readonly declare_incident_link: string; - team: string | null; + teams: components['schemas']['FastTeam'][]; grafana_incident_id?: string | null; readonly labels: components['schemas']['AlertGroupLabel'][]; readonly permalinks: { @@ -1652,7 +1652,7 @@ export interface components { readonly status: number; /** @description Generate a link for AlertGroup to declare Grafana Incident by click */ readonly declare_incident_link: string; - team: string | null; + teams: components['schemas']['FastTeam'][]; grafana_incident_id?: string | null; readonly labels: components['schemas']['AlertGroupLabel'][]; readonly permalinks: { diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index e8787c277b..00b871b2fb 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -778,9 +778,11 @@ class _IncidentsPage extends React.Component - - + record.teams.map((team) => ( + + + + )) ); };