oncall-engine/engine/apps/api/serializers/alert_group.py

248 lines
9.1 KiB
Python
Raw Normal View History

import datetime
import logging
from django.core.cache import cache
from django.utils import timezone
from drf_spectacular.utils import extend_schema_field, inline_serializer
from rest_framework import serializers
2022-07-20 10:26:14 +03:00
from apps.alerts.incident_appearance.renderers.classic_markdown_renderer import AlertGroupClassicMarkdownRenderer
2022-07-20 10:58:54 +03:00
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
Update Slack "invite" feature to use direct paging (#2562) # What this PR does Refactors the "invite" functionality in Slack to use direct paging and be more consistent with the web UI and `/escalate` Slack command. ## Screenshots ### Alert group buttons Before: <img width="609" alt="Screenshot 2023-07-17 at 22 40 47" src="https://github.com/grafana/oncall/assets/20116910/68fad5a4-5011-4d74-b1c7-362bdb4f8cf0"> After (replace "Invite..." dropdown with "Responders" button, swap it with the silence button): <img width="587" alt="Screenshot 2023-07-17 at 22 37 19" src="https://github.com/grafana/oncall/assets/20116910/50b42057-f46b-4558-ab1c-56c34a15af5e"> ### What happens when clicking on "Responders" The following modal opens up with a list of currently paged users and inputs to page more users/schedules: <img width="514" alt="Screenshot 2023-07-17 at 22 37 52" src="https://github.com/grafana/oncall/assets/20116910/70bd2853-d459-4343-8b25-8519ac0098f7"> This is supposed to be the Slack equivalent of this part of the web UI: <img width="601" alt="Screenshot 2023-07-17 at 22 47 17" src="https://github.com/grafana/oncall/assets/20116910/101e1229-a5c4-404f-8388-eceee3e4820f"> ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/2336 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-07-18 09:36:11 +01:00
from apps.alerts.models import AlertGroup
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin
from .alert import AlertSerializer
from .alert_receive_channel import FastAlertReceiveChannelSerializer
from .alerts_field_cache_buster_mixin import AlertsFieldCacheBusterMixin
Add responders improvements (#3128) # What this PR does https://www.loom.com/share/c5e10b5ec51343d0954c6f41cfd6a5fb ## Summary of backend changes - Add `AlertReceiveChannel.get_orgs_direct_paging_integrations` method and `AlertReceiveChannel.is_contactable` property. These are needed to be able to (optionally) filter down teams, in the `GET /teams` internal API endpoint ([here](https://github.com/grafana/oncall/pull/3128/files#diff-a4bd76e557f7e11dafb28a52c1034c075028c693b3c12d702d53c07fc6f24c05R55-R63)), to just teams that have a "contactable" Direct Paging integration - `engine/apps/alerts/paging.py` - update these functions to support new UX. In short `direct_paging` no longer takes a list of `ScheduleNotifications` or an `EscalationChain` object - add `user_is_oncall` helper function - add `_construct_title` helper function. In short if no `title` is provided, which is the case for Direct Pages originating from OnCall (either UI or Slack), then the format is `f"{from_user.username} is paging <team.name (if team is specified> <comma separated list of user.usernames> to join escalation"` - `engine/apps/api/serializers/team.py` - add `number_of_users_currently_oncall` attribute to response schema ([code](https://github.com/grafana/oncall/pull/3128/files#diff-26af48f796c9e987a76447586dd0f92349783d6ea6a0b6039a2f0f28bd58c2ebR45-R52)) - `engine/apps/api/serializers/user.py` - add `is_currently_oncall` attribute to response schema ([code](https://github.com/grafana/oncall/pull/3128/files#diff-6744b5544ebb120437af98a996da5ad7d48ee1139a6112c7e3904010ab98f232R157-R162)) - `engine/apps/api/views/team.py` - add support for two new optional query params `only_include_notifiable_teams` and `include_no_team` ([code](https://github.com/grafana/oncall/pull/3128/files#diff-a4bd76e557f7e11dafb28a52c1034c075028c693b3c12d702d53c07fc6f24c05R55-R70)) - `engine/apps/api/views/user.py` - in the `GET /users` internal API endpoint, when specifying the `search` query param now also search on `teams__name` ([code](https://github.com/grafana/oncall/pull/3128/files#diff-30309629484ad28e6fe09816e1bd226226d652ea977b6f3b6775976c729bf4b5R223); this is a new UX requirement) - add support for a new optional query param, `is_currently_oncall`, to allow filtering users based on.. whether they are currently on call or not ([code](https://github.com/grafana/oncall/pull/3128/files#diff-30309629484ad28e6fe09816e1bd226226d652ea977b6f3b6775976c729bf4b5R272-R282)) - remove `check_availability` endpoint (no longer used with new UX; also removed references in frontend code) - `engine/apps/slack/scenarios/paging.py` and `engine/apps/slack/scenarios/manage_responders.py` - update Slack workflows to support new UX. Schedules are no longer a concept here. When creating a new alert group via `/escalate` the user either specifies a team and/or user(s) (they must specify at least one of the two and validation is done here to check this). When adding responders to an existing alert group it's simply a list of users that they can add, no more schedules. - add `Organization.slack_is_configured` and `Organization.telegram_is_configured` properties. These are needed to support [this new functionality ](https://github.com/grafana/oncall/pull/3128/files#diff-9d96504027309f2bd1e95352bac1433b09b60eb4fafb611b52a6c15ed16cbc48R271-R272) in the `AlertReceiveChannel` model. ## Summary of frontend changes - Refactor/rename `EscalationVariants` component to `AddResponders` + remove `grafana-plugin/src/containers/UserWarningModal` (no longer needed with new UX) - Remove `grafana-plugin/src/models/user.ts` as it seemed to be a duplicate of `grafana-plugin/src/models/user/user.types.ts` Related to https://github.com/grafana/incident/issues/4278 - Closes #3115 - Closes #3116 - Closes #3117 - Closes #3118 - Closes #3177 ## TODO - [x] make frontend changes - [x] update Slack backend functionality - [x] update public documentation - [x] add/update e2e tests ## Post-deploy To-dos - [ ] update dev/ops/production Slack bots to update `/escalate` command description (should now say "Direct page a team or user(s)") ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-10-27 12:12:07 -04:00
from .user import FastUserSerializer, PagedUserSerializer, UserShortSerializer
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class AlertGroupFieldsCacheSerializerMixin(AlertsFieldCacheBusterMixin):
CACHE_KEY_FORMAT_TEMPLATE = "{field_name}_alert_group_{object_id}"
@classmethod
def get_or_set_web_template_field(
cls,
obj,
last_alert,
field_name,
renderer_class,
cache_lifetime=60 * 60 * 24,
):
CACHE_KEY = cls.calculate_cache_key(field_name, obj)
cached_field = cache.get(CACHE_KEY, None)
web_templates_modified_at = obj.channel.web_templates_modified_at
last_alert_created_at = last_alert.created_at
# use cache only if cache exists
# and cache was created after the last alert created
# and either web templates never modified
# or cache was created after templates were modified
if (
cached_field is not None
and cached_field.get("cache_created_at") > last_alert_created_at
and (web_templates_modified_at is None or cached_field.get("cache_created_at") > web_templates_modified_at)
):
field = cached_field.get(field_name)
else:
field = renderer_class(obj, last_alert).render()
cache.set(CACHE_KEY, {"cache_created_at": timezone.now(), field_name: field}, cache_lifetime)
return field
class ShortAlertGroupSerializer(AlertGroupFieldsCacheSerializerMixin, serializers.ModelSerializer):
pk = serializers.CharField(read_only=True, source="public_primary_key")
alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
render_for_web = serializers.SerializerMethodField()
class Meta:
model = AlertGroup
fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
read_only_fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
@extend_schema_field(
inline_serializer(
name="render_for_web",
fields={
"title": serializers.CharField(),
"message": serializers.CharField(),
"image_url": serializers.CharField(),
"source_link": serializers.CharField(),
},
)
)
def get_render_for_web(self, obj):
last_alert = obj.alerts.last()
if last_alert is None:
return {}
return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
obj,
last_alert,
AlertGroupFieldsCacheSerializerMixin.RENDER_FOR_WEB_FIELD_NAME,
AlertGroupWebRenderer,
)
class AlertGroupListSerializer(EagerLoadingMixin, AlertGroupFieldsCacheSerializerMixin, serializers.ModelSerializer):
pk = serializers.CharField(read_only=True, source="public_primary_key")
alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
status = serializers.ReadOnlyField()
resolved_by_user = FastUserSerializer(required=False)
acknowledged_by_user = FastUserSerializer(required=False)
silenced_by_user = FastUserSerializer(required=False)
related_users = serializers.SerializerMethodField()
dependent_alert_groups = ShortAlertGroupSerializer(many=True)
root_alert_group = ShortAlertGroupSerializer()
team = TeamPrimaryKeyRelatedField(source="channel.team", allow_null=True)
alerts_count = serializers.IntegerField(read_only=True)
render_for_web = serializers.SerializerMethodField()
2022-07-20 10:26:14 +03:00
render_for_classic_markdown = serializers.SerializerMethodField()
PREFETCH_RELATED = [
"dependent_alert_groups",
"log_records__author",
]
SELECT_RELATED = [
"channel__organization",
"channel__team",
"root_alert_group",
"resolved_by_user",
"acknowledged_by_user",
"silenced_by_user",
]
class Meta:
model = AlertGroup
fields = [
"pk",
"alerts_count",
"inside_organization_number",
"alert_receive_channel",
"resolved",
"resolved_by",
"resolved_by_user",
"resolved_at",
"acknowledged_at",
"acknowledged",
"acknowledged_on_source",
"acknowledged_at",
"acknowledged_by_user",
"silenced",
"silenced_by_user",
"silenced_at",
"silenced_until",
"started_at",
"silenced_until",
"related_users",
"render_for_web",
2022-07-20 10:26:14 +03:00
"render_for_classic_markdown",
"dependent_alert_groups",
"root_alert_group",
"status",
"declare_incident_link",
"team",
"grafana_incident_id",
]
@extend_schema_field(
inline_serializer(
name="render_for_web",
fields={
"title": serializers.CharField(),
"message": serializers.CharField(),
"image_url": serializers.CharField(),
"source_link": serializers.CharField(),
},
)
)
def get_render_for_web(self, obj):
if not obj.last_alert:
return {}
return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
obj,
obj.last_alert,
AlertGroupFieldsCacheSerializerMixin.RENDER_FOR_WEB_FIELD_NAME,
AlertGroupWebRenderer,
)
2022-07-20 10:26:14 +03:00
def get_render_for_classic_markdown(self, obj):
"""Deprecated. TODO: remove"""
if not obj.last_alert:
return {}
return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
obj,
obj.last_alert,
AlertGroupFieldsCacheSerializerMixin.RENDER_FOR_CLASSIC_MARKDOWN_FIELD_NAME,
AlertGroupClassicMarkdownRenderer,
)
2022-07-20 10:26:14 +03:00
@extend_schema_field(UserShortSerializer(many=True))
def get_related_users(self, obj):
users_ids = set()
users = []
# add resolved and acknowledged by_user explicitly because logs are already prefetched
# when def acknowledge/resolve are called in view.
if obj.resolved_by_user:
users_ids.add(obj.resolved_by_user.public_primary_key)
users.append(obj.resolved_by_user)
if obj.acknowledged_by_user and obj.acknowledged_by_user.public_primary_key not in users_ids:
users_ids.add(obj.acknowledged_by_user.public_primary_key)
users.append(obj.acknowledged_by_user)
if obj.silenced_by_user and obj.silenced_by_user.public_primary_key not in users_ids:
users_ids.add(obj.silenced_by_user.public_primary_key)
users.append(obj.silenced_by_user)
for log_record in obj.log_records.all():
if log_record.author is not None and log_record.author.public_primary_key not in users_ids:
users.append(log_record.author)
users_ids.add(log_record.author.public_primary_key)
return UserShortSerializer(users, many=True).data
class AlertGroupSerializer(AlertGroupListSerializer):
alerts = serializers.SerializerMethodField("get_limited_alerts")
last_alert_at = serializers.SerializerMethodField()
paged_users = serializers.SerializerMethodField()
class Meta(AlertGroupListSerializer.Meta):
fields = AlertGroupListSerializer.Meta.fields + [
"alerts",
"render_after_resolve_report_json",
"slack_permalink", # TODO: make plugin frontend use "permalinks" field to get Slack link
"permalinks",
"last_alert_at",
"paged_users",
]
def get_last_alert_at(self, obj) -> datetime.datetime:
last_alert = obj.alerts.last()
if not last_alert:
return obj.started_at
return last_alert.created_at
@extend_schema_field(AlertSerializer(many=True))
def get_limited_alerts(self, obj):
"""
Overriding default alerts because there are alert_groups with thousands of them.
It's just too slow, we need to cut here.
"""
alerts = obj.alerts.order_by("-pk")[:100]
return AlertSerializer(alerts, many=True).data
Add responders improvements (#3128) # What this PR does https://www.loom.com/share/c5e10b5ec51343d0954c6f41cfd6a5fb ## Summary of backend changes - Add `AlertReceiveChannel.get_orgs_direct_paging_integrations` method and `AlertReceiveChannel.is_contactable` property. These are needed to be able to (optionally) filter down teams, in the `GET /teams` internal API endpoint ([here](https://github.com/grafana/oncall/pull/3128/files#diff-a4bd76e557f7e11dafb28a52c1034c075028c693b3c12d702d53c07fc6f24c05R55-R63)), to just teams that have a "contactable" Direct Paging integration - `engine/apps/alerts/paging.py` - update these functions to support new UX. In short `direct_paging` no longer takes a list of `ScheduleNotifications` or an `EscalationChain` object - add `user_is_oncall` helper function - add `_construct_title` helper function. In short if no `title` is provided, which is the case for Direct Pages originating from OnCall (either UI or Slack), then the format is `f"{from_user.username} is paging <team.name (if team is specified> <comma separated list of user.usernames> to join escalation"` - `engine/apps/api/serializers/team.py` - add `number_of_users_currently_oncall` attribute to response schema ([code](https://github.com/grafana/oncall/pull/3128/files#diff-26af48f796c9e987a76447586dd0f92349783d6ea6a0b6039a2f0f28bd58c2ebR45-R52)) - `engine/apps/api/serializers/user.py` - add `is_currently_oncall` attribute to response schema ([code](https://github.com/grafana/oncall/pull/3128/files#diff-6744b5544ebb120437af98a996da5ad7d48ee1139a6112c7e3904010ab98f232R157-R162)) - `engine/apps/api/views/team.py` - add support for two new optional query params `only_include_notifiable_teams` and `include_no_team` ([code](https://github.com/grafana/oncall/pull/3128/files#diff-a4bd76e557f7e11dafb28a52c1034c075028c693b3c12d702d53c07fc6f24c05R55-R70)) - `engine/apps/api/views/user.py` - in the `GET /users` internal API endpoint, when specifying the `search` query param now also search on `teams__name` ([code](https://github.com/grafana/oncall/pull/3128/files#diff-30309629484ad28e6fe09816e1bd226226d652ea977b6f3b6775976c729bf4b5R223); this is a new UX requirement) - add support for a new optional query param, `is_currently_oncall`, to allow filtering users based on.. whether they are currently on call or not ([code](https://github.com/grafana/oncall/pull/3128/files#diff-30309629484ad28e6fe09816e1bd226226d652ea977b6f3b6775976c729bf4b5R272-R282)) - remove `check_availability` endpoint (no longer used with new UX; also removed references in frontend code) - `engine/apps/slack/scenarios/paging.py` and `engine/apps/slack/scenarios/manage_responders.py` - update Slack workflows to support new UX. Schedules are no longer a concept here. When creating a new alert group via `/escalate` the user either specifies a team and/or user(s) (they must specify at least one of the two and validation is done here to check this). When adding responders to an existing alert group it's simply a list of users that they can add, no more schedules. - add `Organization.slack_is_configured` and `Organization.telegram_is_configured` properties. These are needed to support [this new functionality ](https://github.com/grafana/oncall/pull/3128/files#diff-9d96504027309f2bd1e95352bac1433b09b60eb4fafb611b52a6c15ed16cbc48R271-R272) in the `AlertReceiveChannel` model. ## Summary of frontend changes - Refactor/rename `EscalationVariants` component to `AddResponders` + remove `grafana-plugin/src/containers/UserWarningModal` (no longer needed with new UX) - Remove `grafana-plugin/src/models/user.ts` as it seemed to be a duplicate of `grafana-plugin/src/models/user/user.types.ts` Related to https://github.com/grafana/incident/issues/4278 - Closes #3115 - Closes #3116 - Closes #3117 - Closes #3118 - Closes #3177 ## TODO - [x] make frontend changes - [x] update Slack backend functionality - [x] update public documentation - [x] add/update e2e tests ## Post-deploy To-dos - [ ] update dev/ops/production Slack bots to update `/escalate` command description (should now say "Direct page a team or user(s)") ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-10-27 12:12:07 -04:00
@extend_schema_field(PagedUserSerializer(many=True))
def get_paged_users(self, obj):
Add responders improvements (#3128) # What this PR does https://www.loom.com/share/c5e10b5ec51343d0954c6f41cfd6a5fb ## Summary of backend changes - Add `AlertReceiveChannel.get_orgs_direct_paging_integrations` method and `AlertReceiveChannel.is_contactable` property. These are needed to be able to (optionally) filter down teams, in the `GET /teams` internal API endpoint ([here](https://github.com/grafana/oncall/pull/3128/files#diff-a4bd76e557f7e11dafb28a52c1034c075028c693b3c12d702d53c07fc6f24c05R55-R63)), to just teams that have a "contactable" Direct Paging integration - `engine/apps/alerts/paging.py` - update these functions to support new UX. In short `direct_paging` no longer takes a list of `ScheduleNotifications` or an `EscalationChain` object - add `user_is_oncall` helper function - add `_construct_title` helper function. In short if no `title` is provided, which is the case for Direct Pages originating from OnCall (either UI or Slack), then the format is `f"{from_user.username} is paging <team.name (if team is specified> <comma separated list of user.usernames> to join escalation"` - `engine/apps/api/serializers/team.py` - add `number_of_users_currently_oncall` attribute to response schema ([code](https://github.com/grafana/oncall/pull/3128/files#diff-26af48f796c9e987a76447586dd0f92349783d6ea6a0b6039a2f0f28bd58c2ebR45-R52)) - `engine/apps/api/serializers/user.py` - add `is_currently_oncall` attribute to response schema ([code](https://github.com/grafana/oncall/pull/3128/files#diff-6744b5544ebb120437af98a996da5ad7d48ee1139a6112c7e3904010ab98f232R157-R162)) - `engine/apps/api/views/team.py` - add support for two new optional query params `only_include_notifiable_teams` and `include_no_team` ([code](https://github.com/grafana/oncall/pull/3128/files#diff-a4bd76e557f7e11dafb28a52c1034c075028c693b3c12d702d53c07fc6f24c05R55-R70)) - `engine/apps/api/views/user.py` - in the `GET /users` internal API endpoint, when specifying the `search` query param now also search on `teams__name` ([code](https://github.com/grafana/oncall/pull/3128/files#diff-30309629484ad28e6fe09816e1bd226226d652ea977b6f3b6775976c729bf4b5R223); this is a new UX requirement) - add support for a new optional query param, `is_currently_oncall`, to allow filtering users based on.. whether they are currently on call or not ([code](https://github.com/grafana/oncall/pull/3128/files#diff-30309629484ad28e6fe09816e1bd226226d652ea977b6f3b6775976c729bf4b5R272-R282)) - remove `check_availability` endpoint (no longer used with new UX; also removed references in frontend code) - `engine/apps/slack/scenarios/paging.py` and `engine/apps/slack/scenarios/manage_responders.py` - update Slack workflows to support new UX. Schedules are no longer a concept here. When creating a new alert group via `/escalate` the user either specifies a team and/or user(s) (they must specify at least one of the two and validation is done here to check this). When adding responders to an existing alert group it's simply a list of users that they can add, no more schedules. - add `Organization.slack_is_configured` and `Organization.telegram_is_configured` properties. These are needed to support [this new functionality ](https://github.com/grafana/oncall/pull/3128/files#diff-9d96504027309f2bd1e95352bac1433b09b60eb4fafb611b52a6c15ed16cbc48R271-R272) in the `AlertReceiveChannel` model. ## Summary of frontend changes - Refactor/rename `EscalationVariants` component to `AddResponders` + remove `grafana-plugin/src/containers/UserWarningModal` (no longer needed with new UX) - Remove `grafana-plugin/src/models/user.ts` as it seemed to be a duplicate of `grafana-plugin/src/models/user/user.types.ts` Related to https://github.com/grafana/incident/issues/4278 - Closes #3115 - Closes #3116 - Closes #3117 - Closes #3118 - Closes #3177 ## TODO - [x] make frontend changes - [x] update Slack backend functionality - [x] update public documentation - [x] add/update e2e tests ## Post-deploy To-dos - [ ] update dev/ops/production Slack bots to update `/escalate` command description (should now say "Direct page a team or user(s)") ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-10-27 12:12:07 -04:00
return obj.get_paged_users()