From b3dc7cfc5c9c8e3fde53c7efcb8877a84536a827 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 16 Dec 2024 19:30:58 +0000 Subject: [PATCH 1/9] Release oncall Helm chart 1.13.11 --- helm/oncall/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm/oncall/Chart.yaml b/helm/oncall/Chart.yaml index cd0288390c..3524ab53f7 100644 --- a/helm/oncall/Chart.yaml +++ b/helm/oncall/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: oncall description: Developer-friendly incident response with brilliant Slack integration type: application -version: 1.13.10 -appVersion: v1.13.10 +version: 1.13.11 +appVersion: v1.13.11 dependencies: - name: cert-manager version: v1.8.0 From ad2976b3688de7de646df0c066338a8a98aa907e Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Tue, 3 Dec 2024 09:35:26 +1100 Subject: [PATCH 2/9] WIP --- .../oncall-api-reference/alertgroups.md | 4 + .../migrations/0065_alertgroup_teams.py | 19 +++ .../0066_channelfilter_update_team.py | 18 ++ engine/apps/alerts/models/alert.py | 13 ++ engine/apps/alerts/models/alert_group.py | 2 + .../alerts/models/alert_receive_channel.py | 4 +- engine/apps/alerts/models/channel_filter.py | 3 + engine/apps/api/serializers/alert_group.py | 20 ++- engine/apps/api/serializers/channel_filter.py | 2 + engine/apps/api/views/alert_group.py | 23 ++- engine/apps/metrics_exporter/helpers.py | 147 +++++++++-------- .../metrics_exporter/metrics_cache_manager.py | 10 +- .../metrics_exporter/metrics_collectors.py | 8 +- engine/apps/metrics_exporter/tasks.py | 155 ++++++++++-------- .../public_api/serializers/alert_groups.py | 8 + engine/apps/public_api/serializers/routes.py | 2 + .../ExpandedIntegrationRouteDisplay.tsx | 16 ++ .../channel_filter/channel_filter.types.ts | 1 + .../oncall-api/autogenerated-api.types.d.ts | 4 +- .../src/pages/incidents/Incidents.tsx | 8 +- 20 files changed, 296 insertions(+), 171 deletions(-) create mode 100644 engine/apps/alerts/migrations/0065_alertgroup_teams.py create mode 100644 engine/apps/alerts/migrations/0066_channelfilter_update_team.py diff --git a/docs/sources/oncall-api-reference/alertgroups.md b/docs/sources/oncall-api-reference/alertgroups.md index 2a9c701a09..6377717523 100644 --- a/docs/sources/oncall-api-reference/alertgroups.md +++ b/docs/sources/oncall-api-reference/alertgroups.md @@ -73,6 +73,9 @@ The above command returns JSON structured in the following way: ] } }, + "teams": [ + "TE5EF3RQHJQPI" + ] } ], "current_page_number": 1, @@ -80,6 +83,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..eaeb78a9e2 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -145,6 +145,19 @@ def create( group.log_records.create(type=AlertGroupLogRecord.TYPE_REGISTERED) group.log_records.create(type=AlertGroupLogRecord.TYPE_ROUTE_ASSIGNED) + if group_created and alert_receive_channel.team: + # add the team from the integration if its available + group.teams.set([alert_receive_channel.team]) + elif ( + group_created + and 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 6bcb8d735f..1674ab9aa7 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -425,6 +425,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/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 74fc5d237a..782a59eeb1 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -797,6 +797,6 @@ def listen_for_alertreceivechannel_model_save( # delete connected auth tokens instance.auth_tokens.all().delete() - metrics_remove_deleted_integration_from_cache(instance) + metrics_remove_deleted_integration_from_cache(instance, instance.organization) else: - metrics_update_integration_cache(instance) + metrics_update_integration_cache(instance, instance.organization) diff --git a/engine/apps/alerts/models/channel_filter.py b/engine/apps/alerts/models/channel_filter.py index 0f67f575c8..240488d7cd 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) slack_channel = models.ForeignKey( diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index c0882658fb..0e9a6c744d 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,24 @@ 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: + teams = obj.teams + elif obj.channel.team: + teams = [obj.channel.team] + 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 +244,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/views/alert_group.py b/engine/apps/api/views/alert_group.py index 117fb9ce9d..15080f46d7 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -302,24 +302,21 @@ def get_serializer_class(self): 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 - ) - if not ignore_filtering_by_available_teams: - alert_receive_channels_qs = alert_receive_channels_qs.filter(*self.available_teams_lookup_args) - - # 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]) + null_team_lookup = Q(teams__isnull=True) 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]) 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_ids = list(alert_receive_channels_qs.values_list("id", flat=True)) - queryset = AlertGroup.objects.filter(channel__in=alert_receive_channels_ids) + 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..e1571bbe45 100644 --- a/engine/apps/metrics_exporter/helpers.py +++ b/engine/apps/metrics_exporter/helpers.py @@ -141,47 +141,45 @@ def get_default_states_dict() -> AlertGroupStateDict: } -def metrics_update_integration_cache(integration: "AlertReceiveChannel") -> None: +def metrics_update_integration_cache(integration: "AlertReceiveChannel", organization: "Organization") -> None: """Update integration data in metrics cache""" 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 organization.teams.all(): + 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"): +def metrics_remove_deleted_integration_from_cache(integration: "AlertReceiveChannel", organization: "Organization"): """Remove data related to deleted integration from metrics cache""" 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) - if metric_cache: - metric_cache.pop(integration.id, None) - cache.set(metric_key, metric_cache, timeout=metrics_cache_timeout) + for team in organization.teams.all(): + 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"): """ Bulk add new integration data to metrics cache. This method is safe to call multiple times on the same integrations. """ - 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.org_id) + metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization.org_id) instance_slug = organization.stack_slug instance_id = organization.stack_id @@ -189,40 +187,41 @@ 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 organization.teams.all(): + 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) + metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization.org_id) metric_alert_groups_response_time: typing.Dict[int, AlertGroupsResponseTimeMetricsDict] = cache.get( 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 organization.teams.all(): + 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 +235,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: + for integration_id, team_id in metric_alert_groups_total: + if metric_alert_groups_total[(integration_id, team_id)]["team_id"] == team_id: integration_response_time_metrics = metric_alert_groups_response_time.get(integration_id) 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[(integration_id, team_id)]["team_id"] = "no_team" + metric_alert_groups_total[(integration_id, team_id)]["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[(integration_id, team_id)]["team_name"] = team_data["team_name"] if integration_response_time_metrics: integration_response_time_metrics["team_name"] = team_data["team_name"] @@ -255,7 +257,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,24 +283,27 @@ 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.org_id) + metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization.org_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)) - if not integration_alert_groups: - continue - for service_name, service_state_diff in service_data.items(): - states_to_update = integration_alert_groups["services"].setdefault(service_name, get_default_states_dict()) - for previous_state, counter in service_state_diff["previous_states"].items(): - if states_to_update[previous_state] - counter > 0: - states_to_update[previous_state] -= counter - else: - states_to_update[previous_state] = 0 - for new_state, counter in service_state_diff["new_states"].items(): - states_to_update[new_state] += counter + for team in organization.teams.all(): + integration_alert_groups = metric_alert_groups_total.get((int(integration_id), team.team_id)) + if not integration_alert_groups: + continue + for service_name, service_state_diff in service_data.items(): + states_to_update = integration_alert_groups["services"].setdefault(service_name, get_default_states_dict()) + for previous_state, counter in service_state_diff["previous_states"].items(): + if states_to_update[previous_state] - counter > 0: + states_to_update[previous_state] -= counter + else: + states_to_update[previous_state] = 0 + for new_state, counter in service_state_diff["new_states"].items(): + states_to_update[new_state] += counter cache.set(metric_alert_groups_total_key, metric_alert_groups_total, timeout=metrics_cache_timeout) diff --git a/engine/apps/metrics_exporter/metrics_cache_manager.py b/engine/apps/metrics_exporter/metrics_cache_manager.py index 25f85655f5..fd902aeaf9 100644 --- a/engine/apps/metrics_exporter/metrics_cache_manager.py +++ b/engine/apps/metrics_exporter/metrics_cache_manager.py @@ -54,7 +54,7 @@ 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 + integration_id, organization, service_name, old_state=None, new_state=None ): """ Update state metric cache for one alert group. @@ -62,7 +62,7 @@ def metrics_update_state_cache_for_alert_group( metrics_state_diff = MetricsCacheManager.update_integration_states_diff( {}, integration_id, 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( @@ -79,7 +79,7 @@ def metrics_update_response_time_cache_for_alert_group( @staticmethod def metrics_update_cache_for_alert_group( integration_id, - organization_id, + organization, old_state=None, new_state=None, response_time=None, @@ -91,9 +91,9 @@ def metrics_update_cache_for_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 + integration_id, organization, 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 + integration_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..a238260d0b 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,7 +288,7 @@ 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, 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/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) => ( + + + + )) ); }; From a727114ee03b7f0cd6c55f5191b3d8a55144b5d4 Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Tue, 17 Dec 2024 17:02:57 +1100 Subject: [PATCH 3/9] fixes for most tests --- engine/apps/alerts/models/alert.py | 23 +++-- .../alerts/models/alert_receive_channel.py | 2 +- engine/apps/api/tests/test_alert_group.py | 39 ++++++-- engine/apps/api/tests/test_team.py | 22 ++++- engine/apps/api/views/alert_group.py | 10 +- engine/apps/metrics_exporter/helpers.py | 94 +++++++++++-------- .../metrics_exporter/metrics_cache_manager.py | 33 ++++--- engine/apps/metrics_exporter/tasks.py | 1 + .../tests/test_calculation_metrics.py | 8 +- .../tests/test_update_metrics_cache.py | 82 ++++++++++------ .../public_api/tests/test_alert_groups.py | 2 +- .../apps/public_api/tests/test_escalation.py | 1 + .../public_api/tests/test_integrations.py | 9 ++ engine/apps/public_api/tests/test_routes.py | 9 ++ 14 files changed, 218 insertions(+), 117 deletions(-) diff --git a/engine/apps/alerts/models/alert.py b/engine/apps/alerts/models/alert.py index eaeb78a9e2..0cc3e7bfdb 100644 --- a/engine/apps/alerts/models/alert.py +++ b/engine/apps/alerts/models/alert.py @@ -145,18 +145,17 @@ def create( group.log_records.create(type=AlertGroupLogRecord.TYPE_REGISTERED) group.log_records.create(type=AlertGroupLogRecord.TYPE_ROUTE_ASSIGNED) - if group_created and alert_receive_channel.team: - # add the team from the integration if its available - group.teams.set([alert_receive_channel.team]) - elif ( - group_created - and 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 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 diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 782a59eeb1..a7d212a2e3 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -799,4 +799,4 @@ def listen_for_alertreceivechannel_model_save( metrics_remove_deleted_integration_from_cache(instance, instance.organization) else: - metrics_update_integration_cache(instance, instance.organization) + metrics_update_integration_cache(instance) diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 8ee438b6bc..1994860dfa 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") @@ -911,6 +933,7 @@ def test_get_filter_by_teams( # check the "No team" case response = client.get(url + "?team=null", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK + print() assert len(response.data["results"]) == 1 assert {ag["pk"] for ag in response.data["results"]} == {alert_group_0.public_primary_key} 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 15080f46d7..e44a858eb6 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: @@ -293,6 +293,8 @@ class AlertGroupView( filter_backends = [AlertGroupSearchFilter, filters.DjangoFilterBackend] filterset_class = AlertGroupFilter + TEAM_LOOKUP="teams" + def get_serializer_class(self): if self.action == "list": return AlertGroupListSerializer @@ -304,11 +306,12 @@ def get_queryset(self, ignore_filtering_by_available_teams=False): team_values = self.request.query_params.getlist("team", []) if team_values: - null_team_lookup = Q(teams__isnull=True) if NO_TEAM_VALUE in team_values else None + null_team_lookup = (Q(teams__isnull=True) | Q(teams=None)) 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]) 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: @@ -316,7 +319,6 @@ def get_queryset(self, ignore_filtering_by_available_teams=False): 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 e1571bbe45..e0b22338ae 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 @@ -141,19 +148,26 @@ def get_default_states_dict() -> AlertGroupStateDict: } -def metrics_update_integration_cache(integration: "AlertReceiveChannel", organization: "Organization") -> None: +def metrics_update_integration_cache(integration: "AlertReceiveChannel") -> None: """Update integration data in metrics cache""" - 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 team in organization.teams.all(): + 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) + print(integration.organization.id) + print("cccc") + for team in _get_teams_for_cache(integration.organization): + print(team) for metric_key in [metric_alert_groups_total_key, metric_alert_groups_response_time_key]: + print(metric_key) metric_cache = cache.get(metric_key, {}) + print(metric_cache) integration_metric_cache = metric_cache.get((integration.id,team.team_id)) + print(integration_metric_cache) if integration_metric_cache: cache_updated = False + print("eeee") if integration_metric_cache["integration_name"] != integration.emojized_verbal_name: + print("dddd") integration_metric_cache["integration_name"] = integration.emojized_verbal_name cache_updated = True if cache_updated: @@ -163,10 +177,10 @@ def metrics_update_integration_cache(integration: "AlertReceiveChannel", organiz def metrics_remove_deleted_integration_from_cache(integration: "AlertReceiveChannel", organization: "Organization"): """Remove data related to deleted integration from metrics cache""" 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_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 team in organization.teams.all(): + for team in _get_teams_for_cache(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: @@ -178,8 +192,8 @@ def metrics_add_integrations_to_cache(integrations: list["AlertReceiveChannel"], """ Bulk add new integration data to metrics cache. This method is safe to call multiple times on the same integrations. """ - metrics_cache_timeout = get_metrics_cache_timeout(organization.org_id) - metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization.org_id) + metrics_cache_timeout = get_metrics_cache_timeout(organization.id) + metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization.id) instance_slug = organization.stack_slug instance_id = organization.stack_id @@ -187,7 +201,7 @@ 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 team in organization.teams.all(): + for team in _get_teams_for_cache(organization): for integration in integrations: metric_alert_groups_total.setdefault( (integration.id,team.team_id), @@ -203,12 +217,12 @@ def metrics_add_integrations_to_cache(integrations: list["AlertReceiveChannel"], ) 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.org_id) + metric_alert_groups_response_time_key = get_metric_alert_groups_response_time_key(organization.id) metric_alert_groups_response_time: typing.Dict[int, AlertGroupsResponseTimeMetricsDict] = cache.get( metric_alert_groups_response_time_key, {} ) - for team in organization.teams.all(): + for team in _get_teams_for_cache(organization): for integration in integrations: metric_alert_groups_response_time.setdefault( (integration.id, team.team_id), @@ -239,17 +253,17 @@ def metrics_bulk_update_team_label_cache(teams_updated_data: dict, organization_ # 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, team_id in metric_alert_groups_total: - if metric_alert_groups_total[(integration_id, team_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)]["team_id"] = "no_team" - metric_alert_groups_total[(integration_id, team_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_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"] @@ -283,32 +297,31 @@ def metrics_update_alert_groups_state_cache(states_diff: dict, organization: "Or if not states_diff: return - metrics_cache_timeout = get_metrics_cache_timeout(organization.org_id) - metric_alert_groups_total_key = get_metric_alert_groups_total_key(organization.org_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(): - for team in organization.teams.all(): - integration_alert_groups = metric_alert_groups_total.get((int(integration_id), team.team_id)) - if not integration_alert_groups: - continue - for service_name, service_state_diff in service_data.items(): - states_to_update = integration_alert_groups["services"].setdefault(service_name, get_default_states_dict()) - for previous_state, counter in service_state_diff["previous_states"].items(): - if states_to_update[previous_state] - counter > 0: - states_to_update[previous_state] -= counter - else: - states_to_update[previous_state] = 0 - for new_state, counter in service_state_diff["new_states"].items(): - states_to_update[new_state] += counter + 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(): + states_to_update = integration_alert_groups["services"].setdefault(service_name, get_default_states_dict()) + for previous_state, counter in service_state_diff["previous_states"].items(): + if states_to_update[previous_state] - counter > 0: + states_to_update[previous_state] -= counter + else: + states_to_update[previous_state] = 0 + for new_state, counter in service_state_diff["new_states"].items(): + states_to_update[new_state] += counter 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: @@ -320,14 +333,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 fd902aeaf9..94a86a7897 100644 --- a/engine/apps/metrics_exporter/metrics_cache_manager.py +++ b/engine/apps/metrics_exporter/metrics_cache_manager.py @@ -54,27 +54,27 @@ def update_integration_states_diff(metrics_dict, integration_id, service_name, p @staticmethod def metrics_update_state_cache_for_alert_group( - integration_id, organization, 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) @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( @@ -85,15 +85,18 @@ def metrics_update_cache_for_alert_group( 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, response_time_seconds, service_name - ) - if old_state or new_state: - MetricsCacheManager.metrics_update_state_cache_for_alert_group( - integration_id, organization, 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/tasks.py b/engine/apps/metrics_exporter/tasks.py index a238260d0b..f68b50e904 100644 --- a/engine/apps/metrics_exporter/tasks.py +++ b/engine/apps/metrics_exporter/tasks.py @@ -294,6 +294,7 @@ def update_metrics_for_alert_group(alert_group_id, organization_id, previous_sta 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/tests/test_alert_groups.py b/engine/apps/public_api/tests/test_alert_groups.py index 247246ddca..496857c8eb 100644 --- a/engine/apps/public_api/tests/test_alert_groups.py +++ b/engine/apps/public_api/tests/test_alert_groups.py @@ -13,7 +13,6 @@ from apps.api import permissions from apps.auth_token.tests.helpers import setup_service_account_api_mocks - def construct_expected_response_from_alert_groups(alert_groups): results = [] for alert_group in alert_groups: @@ -57,6 +56,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 c2ce4cf738..c03bbd20be 100644 --- a/engine/apps/public_api/tests/test_integrations.py +++ b/engine/apps/public_api/tests/test_integrations.py @@ -44,6 +44,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/", @@ -313,6 +314,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/", @@ -376,6 +378,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/", @@ -455,6 +458,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/", @@ -566,6 +570,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/", @@ -629,6 +634,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/", @@ -692,6 +698,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/", @@ -758,6 +765,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/", @@ -827,6 +835,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 From b55993c8e4fb285999b3b3e43b008f4fdcd4abe9 Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Thu, 19 Dec 2024 10:17:29 +1100 Subject: [PATCH 4/9] WIP --- .../alerts/models/alert_receive_channel.py | 2 +- engine/apps/api/tests/test_alert_group.py | 1 - engine/apps/metrics_exporter/helpers.py | 20 ++++++------------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index a7d212a2e3..74fc5d237a 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -797,6 +797,6 @@ def listen_for_alertreceivechannel_model_save( # delete connected auth tokens instance.auth_tokens.all().delete() - metrics_remove_deleted_integration_from_cache(instance, instance.organization) + metrics_remove_deleted_integration_from_cache(instance) else: metrics_update_integration_cache(instance) diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 1994860dfa..61a72e9334 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -933,7 +933,6 @@ def test_get_filter_by_teams( # check the "No team" case response = client.get(url + "?team=null", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK - print() assert len(response.data["results"]) == 1 assert {ag["pk"] for ag in response.data["results"]} == {alert_group_0.public_primary_key} diff --git a/engine/apps/metrics_exporter/helpers.py b/engine/apps/metrics_exporter/helpers.py index e0b22338ae..50e0dc413e 100644 --- a/engine/apps/metrics_exporter/helpers.py +++ b/engine/apps/metrics_exporter/helpers.py @@ -150,37 +150,29 @@ def get_default_states_dict() -> AlertGroupStateDict: def metrics_update_integration_cache(integration: "AlertReceiveChannel") -> None: """Update integration data in metrics cache""" - 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) - print(integration.organization.id) - print("cccc") + 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 team in _get_teams_for_cache(integration.organization): - print(team) for metric_key in [metric_alert_groups_total_key, metric_alert_groups_response_time_key]: - print(metric_key) metric_cache = cache.get(metric_key, {}) - print(metric_cache) integration_metric_cache = metric_cache.get((integration.id,team.team_id)) - print(integration_metric_cache) if integration_metric_cache: cache_updated = False - print("eeee") if integration_metric_cache["integration_name"] != integration.emojized_verbal_name: - print("dddd") 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", organization: "Organization"): +def metrics_remove_deleted_integration_from_cache(integration: "AlertReceiveChannel"): """Remove data related to deleted integration from metrics cache""" 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_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 team in _get_teams_for_cache(organization): + 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: From ca689e38ef4fd601b3f62f2276216591b7c4fea9 Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Thu, 19 Dec 2024 15:02:08 +1100 Subject: [PATCH 5/9] more fixes --- engine/apps/api/serializers/alert_group.py | 4 ++- engine/apps/api/views/alert_group.py | 32 +++++++++++++++++++--- engine/common/api_helpers/mixins.py | 16 +++++++---- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/engine/apps/api/serializers/alert_group.py b/engine/apps/api/serializers/alert_group.py index 0e9a6c744d..849735f06a 100644 --- a/engine/apps/api/serializers/alert_group.py +++ b/engine/apps/api/serializers/alert_group.py @@ -200,10 +200,12 @@ def get_teams(self, obj: "AlertGroup"): Handle AlertGroups that haven't been assigned a team yet """ - if obj.teams: + 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: diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index e44a858eb6..4bb9a01e93 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -293,8 +293,6 @@ class AlertGroupView( filter_backends = [AlertGroupSearchFilter, filters.DjangoFilterBackend] filterset_class = AlertGroupFilter - TEAM_LOOKUP="teams" - def get_serializer_class(self): if self.action == "list": return AlertGroupListSerializer @@ -304,10 +302,36 @@ def get_serializer_class(self): 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 + ) + if not ignore_filtering_by_available_teams: + 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 + 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: + 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)) + print(alert_receive_channels_ids) + + if team_values: - null_team_lookup = (Q(teams__isnull=True) | Q(teams=None)) 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]) + # 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 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) From 69c6b3b244215af654f34f3a560538fda09b75a0 Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Mon, 6 Jan 2025 10:54:23 +1100 Subject: [PATCH 6/9] Remove test for changing team as we have stats for each team per integration now --- .../tests/test_update_metrics_cache.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) 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 f9250461bb..4e583f5b3d 100644 --- a/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py +++ b/engine/apps/metrics_exporter/tests/test_update_metrics_cache.py @@ -304,15 +304,15 @@ def test_update_metrics_cache_on_update_integration( stack_slug=METRICS_TEST_INSTANCE_SLUG, stack_id=METRICS_TEST_INSTANCE_ID, ) - team = make_team(organization) + # team = make_team(organization) 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) - expected_result_updated_team = { - "team_name": team.name, - "team_id": team.id, - } + # expected_result_updated_team = { + # "team_name": team.name, + # "team_id": team.id, + # } expected_result_updated_name = {"integration_name": "Renamed test integration"} @@ -337,7 +337,6 @@ 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,"no_team"): { @@ -356,10 +355,10 @@ def get_called_arg_index_and_compare_results(): }, }, }, - (alert_receive_channel.id,team.team_id): { + (alert_receive_channel.id,"no_team"): { "integration_name": METRICS_TEST_INTEGRATION_NAME, - "team_name": team.name, - "team_id": team.team_id, + "team_name": "No team", + "team_id": "no_team", "org_id": organization.org_id, "slug": organization.stack_slug, "id": organization.stack_id, @@ -383,10 +382,10 @@ def get_called_arg_index_and_compare_results(): "id": organization.stack_id, "services": {NO_SERVICE_VALUE: []}, }, - (alert_receive_channel.id,team.team_id): { + (alert_receive_channel.id,"no_team"): { "integration_name": METRICS_TEST_INTEGRATION_NAME, - "team_name": team.name, - "team_id": team.team_id, + "team_name": "No team", + "team_id": "no_team", "org_id": organization.org_id, "slug": organization.stack_slug, "id": organization.stack_id, @@ -401,20 +400,24 @@ def get_called_arg_index_and_compare_results(): monkeypatch.setattr(cache, "get", metrics_cache) # check cache update on update integration's team - alert_receive_channel.team = team + # alert_receive_channel.team = team # clear cached_property # del alert_receive_channel.team_name # del alert_receive_channel.team_id_or_no_team - alert_receive_channel.save() + # alert_receive_channel.save() + + # these tests don't make sense any more because we have multiple integrations per team now + # 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) + # expected_result[(alert_receive_channel.id,team.team_id)].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() alert_receive_channel.verbal_name = expected_result_updated_name["integration_name"] # clear cached_property @@ -450,7 +453,7 @@ def get_called_arg_index_and_compare_results(): expected_result_metric_alert_groups_total, expected_result_metric_alert_groups_response_time, ]: - expected_result.pop(alert_receive_channel.id) + expected_result.pop((alert_receive_channel.id,"no_team")) get_called_arg_index_and_compare_results() From 986a779c45eddf9bd196e7324be86d481370d330 Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Mon, 6 Jan 2025 11:01:39 +1100 Subject: [PATCH 7/9] Remove TODO --- engine/apps/metrics_exporter/helpers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/engine/apps/metrics_exporter/helpers.py b/engine/apps/metrics_exporter/helpers.py index 50e0dc413e..b39dd819df 100644 --- a/engine/apps/metrics_exporter/helpers.py +++ b/engine/apps/metrics_exporter/helpers.py @@ -241,8 +241,6 @@ 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 index in metric_alert_groups_total: From a61550ff7a4e813ce4b2f9aba6d2c4be98ab7994 Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Mon, 6 Jan 2025 12:51:22 +1100 Subject: [PATCH 8/9] add tests for channel filter changes --- .../apps/alerts/tests/test_channel_filter.py | 97 ++++++++++++++++++- engine/apps/api/tests/test_alert_group.py | 42 ++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/engine/apps/alerts/tests/test_channel_filter.py b/engine/apps/alerts/tests/test_channel_filter.py index b6079449c9..2c1b88f7bf 100644 --- a/engine/apps/alerts/tests/test_channel_filter.py +++ b/engine/apps/alerts/tests/test_channel_filter.py @@ -1,6 +1,6 @@ import pytest -from apps.alerts.models import ChannelFilter +from apps.alerts.models import ChannelFilter, Alert @pytest.mark.django_db @@ -216,3 +216,98 @@ def test_slack_channel_or_org_default_none( # Assert that slack_channel_or_org_default returns None assert channel_filter.slack_channel_or_org_default is None +@pytest.mark.django_db +def test_channel_filter_team_update( + make_organization_and_user_with_plugin_token, + make_team, + make_escalation_chain, + make_alert_receive_channel, + make_channel_filter, +): + organization, user, token = make_organization_and_user_with_plugin_token() + team = make_team(organization) + + alert_receive_channel = make_alert_receive_channel(organization) + + escalation_chain = make_escalation_chain(organization=organization) + escalation_chain.team = team + + channel_filter = make_channel_filter(alert_receive_channel, escalation_chain=escalation_chain, is_default=True) + + channel_filter.update_team = True + + 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, + channel_filter=channel_filter + ).group + + assert list(alert_group.teams.all()) == [team] + +@pytest.mark.django_db +def test_channel_filter_no_team_set( + make_organization_and_user_with_plugin_token, + make_team, + make_escalation_chain, + make_alert_receive_channel, + make_channel_filter, +): + organization, user, token = make_organization_and_user_with_plugin_token() + team = make_team(organization) + + alert_receive_channel = make_alert_receive_channel(organization, team=team) + + escalation_chain = make_escalation_chain(organization=organization) + + channel_filter = make_channel_filter(alert_receive_channel, escalation_chain=escalation_chain, is_default=True) + + 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 + + assert list(alert_group.teams.all()) == [team] + +@pytest.mark.django_db +def test_channel_filter_no_escalation_chain( + make_organization_and_user_with_plugin_token, + make_team, + make_escalation_chain, + make_alert_receive_channel, + make_channel_filter, +): + organization, user, token = make_organization_and_user_with_plugin_token() + team = make_team(organization) + + alert_receive_channel = make_alert_receive_channel(organization) + + escalation_chain = make_escalation_chain(organization=organization) + escalation_chain.team = team + + make_channel_filter(alert_receive_channel, escalation_chain=escalation_chain, is_default=True) + channel_filter = make_channel_filter(alert_receive_channel, is_default=True) + + channel_filter.update_team = True + + 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, + channel_filter=channel_filter + ).group + + assert list(alert_group.teams.all()) == [] diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 61a72e9334..be7348cfb2 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -2435,3 +2435,45 @@ def test_filter_default_started_at( ) assert response.status_code == status.HTTP_200_OK assert response.json()["pk"] == old_alert_group.public_primary_key + + + +@pytest.mark.django_db +def test_update_team( + make_organization_and_user_with_plugin_token, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_user_auth_headers, + make_escalation_chain, + make_team, + make_channel_filter +): + organization, user, token = make_organization_and_user_with_plugin_token() + + team = make_team(organization) + + alert_receive_channel = make_alert_receive_channel(organization) + + escalation_chain = make_escalation_chain(organization=organization, team=team) + + channel_filter = make_channel_filter(alert_receive_channel, escalation_chain=escalation_chain, is_default=True, update_team=True) + + + 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 + + client = APIClient() + url = reverse("api-internal:alertgroup-detail", kwargs={"pk": alert_group.public_primary_key}) + + response = client.get(url, **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + assert response.json()['teams'][0]["id"] == team.public_primary_key From fec671b88005bbd47d1dc5ef0b125a985faef006 Mon Sep 17 00:00:00 2001 From: Michaela Wheeler Date: Mon, 6 Jan 2025 12:56:23 +1100 Subject: [PATCH 9/9] remove todo line --- engine/apps/api/views/alert_group.py | 1 - 1 file changed, 1 deletion(-) diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 4bb9a01e93..de10889a73 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -335,7 +335,6 @@ def get_queryset(self, ignore_filtering_by_available_teams=False): 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: