feat: add filter affected services internal endpoint (#5415)
Related to https://github.com/grafana/oncall-private/issues/2977 e.g. `GET /api/plugins/grafana-oncall-app/resources/alertgroups/filter_affected_services?service=service-a&service=service-b` ``` [ {"name": "service-a", "service_url": "http://localhost:3000/a/grafana-slo-app/service/service-a", "alert_groups_url": "http://localhost:3000/a/grafana-oncall-app/alert-groups?status=0&status=1&started_at=now-7d_now&label=service_name:service-a"} ] ```
This commit is contained in:
parent
4c92826c26
commit
2a87bea6ed
5 changed files with 129 additions and 15 deletions
|
|
@ -2413,3 +2413,44 @@ 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_alert_group_affected_services(
|
||||
alert_group_internal_api_setup,
|
||||
make_user_for_organization,
|
||||
make_user_auth_headers,
|
||||
make_alert_group_label_association,
|
||||
):
|
||||
_, token, alert_groups = alert_group_internal_api_setup
|
||||
resolved_ag, ack_ag, new_ag, silenced_ag = alert_groups
|
||||
organization = new_ag.channel.organization
|
||||
user = make_user_for_organization(organization)
|
||||
|
||||
# set firing alert group service label
|
||||
make_alert_group_label_association(organization, new_ag, key_name="service_name", value_name="service-a")
|
||||
# set other service name labels for other alert groups
|
||||
make_alert_group_label_association(organization, ack_ag, key_name="service_name", value_name="service-2")
|
||||
make_alert_group_label_association(organization, resolved_ag, key_name="service_name", value_name="service-3")
|
||||
make_alert_group_label_association(organization, silenced_ag, key_name="service_name", value_name="service-4")
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:alertgroup-filter-affected-services")
|
||||
|
||||
url = f"{url}?service=service-1&service=service-2&service=service-3&service=service-a"
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
expected = [
|
||||
{
|
||||
"name": "service-2",
|
||||
"service_url": "a/grafana-slo-app/service/service-2",
|
||||
"alert_groups_url": "a/grafana-oncall-app/alert-groups?status=0&status=1&started_at=now-7d_now&label=service_name:service-2",
|
||||
},
|
||||
{
|
||||
"name": "service-a",
|
||||
"service_url": "a/grafana-slo-app/service/service-a",
|
||||
"alert_groups_url": "a/grafana-oncall-app/alert-groups?status=0&status=1&started_at=now-7d_now&label=service_name:service-a",
|
||||
},
|
||||
]
|
||||
assert response.json() == expected
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from apps.api.serializers.alert_group_escalation_snapshot import AlertGroupEscal
|
|||
from apps.api.serializers.team import TeamSerializer
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.base.models.user_notification_policy_log_record import UserNotificationPolicyLogRecord
|
||||
from apps.grafana_plugin.ui_url_builder import UIURLBuilder
|
||||
from apps.labels.utils import is_labels_feature_enabled
|
||||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
||||
from apps.user_management.models import Team, User
|
||||
|
|
@ -283,6 +284,7 @@ class AlertGroupView(
|
|||
"bulk_action": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
||||
"preview_template": [RBACPermission.Permissions.INTEGRATIONS_TEST],
|
||||
"escalation_snapshot": [RBACPermission.Permissions.ALERT_GROUPS_READ],
|
||||
"filter_affected_services": [RBACPermission.Permissions.ALERT_GROUPS_READ],
|
||||
}
|
||||
|
||||
queryset = AlertGroup.objects.none() # needed for drf-spectacular introspection
|
||||
|
|
@ -299,9 +301,18 @@ class AlertGroupView(
|
|||
|
||||
return super().get_serializer_class()
|
||||
|
||||
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.
|
||||
|
||||
def _get_queryset(
|
||||
self,
|
||||
action=None,
|
||||
ignore_filtering_by_available_teams=False,
|
||||
team_values=None,
|
||||
started_at=None,
|
||||
label_query=None,
|
||||
):
|
||||
# make base get_queryset reusable via params
|
||||
if action is None:
|
||||
# assume stats by default
|
||||
action = "stats"
|
||||
alert_receive_channels_qs = AlertReceiveChannel.objects_with_deleted.filter(
|
||||
organization_id=self.request.auth.organization.id
|
||||
)
|
||||
|
|
@ -310,7 +321,6 @@ class AlertGroupView(
|
|||
|
||||
# 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])
|
||||
|
|
@ -321,10 +331,10 @@ class AlertGroupView(
|
|||
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 self.action in ("list", "stats") and not self.request.query_params.get("started_at"):
|
||||
if action in ("list", "stats") and not started_at:
|
||||
queryset = queryset.filter(started_at__gte=timezone.now() - timezone.timedelta(days=30))
|
||||
|
||||
if self.action in ("list", "stats") and settings.ALERT_GROUPS_DISABLE_PREFER_ORDERING_INDEX:
|
||||
if action in ("list", "stats") and settings.ALERT_GROUPS_DISABLE_PREFER_ORDERING_INDEX:
|
||||
# workaround related to MySQL "ORDER BY LIMIT Query Optimizer Bug"
|
||||
# read more: https://hackmysql.com/infamous-order-by-limit-query-optimizer-bug/
|
||||
from django_mysql.models import add_QuerySetMixin
|
||||
|
|
@ -333,18 +343,28 @@ class AlertGroupView(
|
|||
queryset = queryset.force_index("alert_group_list_index")
|
||||
|
||||
# Filter by labels. Since alert group labels are "static" filter by names, not IDs.
|
||||
label_query = self.request.query_params.getlist("label", [])
|
||||
kv_pairs = parse_label_query(label_query)
|
||||
for key, value in kv_pairs:
|
||||
# Utilize (organization, key_name, value_name, alert_group) index on AlertGroupAssociatedLabel
|
||||
queryset = queryset.filter(
|
||||
labels__organization=self.request.auth.organization,
|
||||
labels__key_name=key,
|
||||
labels__value_name=value,
|
||||
)
|
||||
if label_query:
|
||||
kv_pairs = parse_label_query(label_query)
|
||||
for key, value in kv_pairs:
|
||||
# Utilize (organization, key_name, value_name, alert_group) index on AlertGroupAssociatedLabel
|
||||
queryset = queryset.filter(
|
||||
labels__organization=self.request.auth.organization,
|
||||
labels__key_name=key,
|
||||
labels__value_name=value,
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
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.
|
||||
return self._get_queryset(
|
||||
action=self.action,
|
||||
ignore_filtering_by_available_teams=ignore_filtering_by_available_teams,
|
||||
team_values=self.request.query_params.getlist("team", []),
|
||||
started_at=self.request.query_params.get("started_at"),
|
||||
label_query=self.request.query_params.getlist("label", []),
|
||||
)
|
||||
|
||||
def get_object(self):
|
||||
obj = super().get_object()
|
||||
obj = self.enrich([obj])[0]
|
||||
|
|
@ -881,3 +901,46 @@ class AlertGroupView(
|
|||
escalation_snapshot = alert_group.escalation_snapshot
|
||||
result = AlertGroupEscalationSnapshotAPISerializer(escalation_snapshot).data if escalation_snapshot else {}
|
||||
return Response(result)
|
||||
|
||||
@extend_schema(
|
||||
responses=inline_serializer(
|
||||
name="AffectedServices",
|
||||
fields={
|
||||
"name": serializers.CharField(),
|
||||
"service_url": serializers.CharField(),
|
||||
"alert_groups_url": serializers.CharField(),
|
||||
},
|
||||
many=True,
|
||||
)
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def filter_affected_services(self, request):
|
||||
"""Given a list of service names, return the ones that have active alerts."""
|
||||
organization = self.request.auth.organization
|
||||
services = self.request.query_params.getlist("service", [])
|
||||
url_builder = UIURLBuilder(organization)
|
||||
affected_services = []
|
||||
days_to_check = 7
|
||||
for service_name in services:
|
||||
is_affected = (
|
||||
self._get_queryset(
|
||||
started_at=timezone.now() - timezone.timedelta(days=days_to_check),
|
||||
label_query=[f"service_name:{service_name}"],
|
||||
)
|
||||
.filter(
|
||||
resolved=False,
|
||||
silenced=False,
|
||||
)
|
||||
.exists()
|
||||
)
|
||||
if is_affected:
|
||||
affected_services.append(
|
||||
{
|
||||
"name": service_name,
|
||||
"service_url": url_builder.service_page(service_name),
|
||||
"alert_groups_url": url_builder.alert_groups(
|
||||
f"?status=0&status=1&started_at=now-{days_to_check}d_now&label=service_name:{service_name}"
|
||||
),
|
||||
}
|
||||
)
|
||||
return Response(affected_services)
|
||||
|
|
|
|||
|
|
@ -103,3 +103,9 @@ def test_build_url_overriden_base_url(org_setup):
|
|||
@pytest.mark.django_db
|
||||
def test_build_url_works_for_irm_and_oncall_plugins(org_setup, is_grafana_irm_enabled, expected_url):
|
||||
assert UIURLBuilder(org_setup(is_grafana_irm_enabled)).alert_group_detail(ALERT_GROUP_ID) == expected_url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_build_url_service_detail_page(org_setup):
|
||||
builder = UIURLBuilder(org_setup())
|
||||
assert builder.service_page("service-a") == f"{GRAFANA_URL}/a/{PluginID.SLO}/service/service-a"
|
||||
|
|
|
|||
|
|
@ -56,3 +56,6 @@ class UIURLBuilder:
|
|||
|
||||
def declare_incident(self, path_extra: str = "") -> str:
|
||||
return self._build_url("incidents/declare", path_extra, plugin_id=PluginID.INCIDENT)
|
||||
|
||||
def service_page(self, service_name: str, path_extra: str = "") -> str:
|
||||
return self._build_url(f"service/{service_name}", path_extra, plugin_id=PluginID.SLO)
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ class PluginID:
|
|||
INCIDENT = "grafana-incident-app"
|
||||
LABELS = "grafana-labels-app"
|
||||
ML = "grafana-ml-app"
|
||||
SLO = "grafana-slo-app"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue