2023-08-16 14:13:56 +08:00
|
|
|
import datetime
|
2022-06-03 08:09:47 -06:00
|
|
|
import logging
|
2023-11-16 07:15:05 -05:00
|
|
|
import typing
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-01-28 12:50:41 +08:00
|
|
|
from django.core.cache import cache
|
2024-07-25 18:26:05 +01:00
|
|
|
from django.db.models import Prefetch
|
2023-01-28 12:50:41 +08:00
|
|
|
from django.utils import timezone
|
2024-01-12 15:11:22 +00:00
|
|
|
from drf_spectacular.utils import extend_schema_field
|
2022-06-03 08:09:47 -06:00
|
|
|
from rest_framework import serializers
|
|
|
|
|
|
2022-07-20 10:58:54 +03:00
|
|
|
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
|
2023-07-18 09:36:11 +01:00
|
|
|
from apps.alerts.models import AlertGroup
|
2023-11-16 07:15:05 -05:00
|
|
|
from apps.alerts.models.alert_group import PagedUser
|
2024-07-25 18:26:05 +01:00
|
|
|
from apps.slack.models import SlackMessage
|
|
|
|
|
from apps.telegram.models import TelegramMessage
|
2023-03-22 00:57:20 +08:00
|
|
|
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
|
2022-06-03 08:09:47 -06:00
|
|
|
from common.api_helpers.mixins import EagerLoadingMixin
|
|
|
|
|
|
|
|
|
|
from .alert import AlertSerializer
|
|
|
|
|
from .alert_receive_channel import FastAlertReceiveChannelSerializer
|
2023-04-14 09:15:57 +02:00
|
|
|
from .alerts_field_cache_buster_mixin import AlertsFieldCacheBusterMixin
|
2023-11-16 07:15:05 -05:00
|
|
|
from .user import FastUserSerializer, UserShortSerializer
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2023-01-28 12:50:41 +08:00
|
|
|
logger.setLevel(logging.DEBUG)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
2024-04-15 14:49:51 -03:00
|
|
|
class ExternalURL(typing.TypedDict):
|
|
|
|
|
integration: str
|
|
|
|
|
integration_type: str
|
|
|
|
|
external_id: str
|
|
|
|
|
url: str
|
|
|
|
|
|
|
|
|
|
|
2024-01-12 15:11:22 +00:00
|
|
|
class RenderForWeb(typing.TypedDict):
|
|
|
|
|
title: str
|
|
|
|
|
message: str
|
|
|
|
|
image_url: str | None
|
|
|
|
|
source_link: str | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EmptyRenderForWeb(typing.TypedDict):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2023-04-14 09:15:57 +02:00
|
|
|
class AlertGroupFieldsCacheSerializerMixin(AlertsFieldCacheBusterMixin):
|
|
|
|
|
CACHE_KEY_FORMAT_TEMPLATE = "{field_name}_alert_group_{object_id}"
|
|
|
|
|
|
2023-02-02 11:37:52 +08:00
|
|
|
@classmethod
|
|
|
|
|
def get_or_set_web_template_field(
|
|
|
|
|
cls,
|
|
|
|
|
obj,
|
2023-02-03 19:53:35 +08:00
|
|
|
last_alert,
|
2023-02-02 11:37:52 +08:00
|
|
|
field_name,
|
|
|
|
|
renderer_class,
|
|
|
|
|
cache_lifetime=60 * 60 * 24,
|
|
|
|
|
):
|
2023-04-14 09:15:57 +02:00
|
|
|
CACHE_KEY = cls.calculate_cache_key(field_name, obj)
|
2023-02-02 11:37:52 +08:00
|
|
|
cached_field = cache.get(CACHE_KEY, None)
|
|
|
|
|
|
|
|
|
|
web_templates_modified_at = obj.channel.web_templates_modified_at
|
2023-02-03 19:53:35 +08:00
|
|
|
last_alert_created_at = last_alert.created_at
|
2023-02-02 11:37:52 +08:00
|
|
|
|
|
|
|
|
# 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:
|
2023-02-03 19:53:35 +08:00
|
|
|
field = renderer_class(obj, last_alert).render()
|
2023-02-02 11:37:52 +08:00
|
|
|
cache.set(CACHE_KEY, {"cache_created_at": timezone.now(), field_name: field}, cache_lifetime)
|
|
|
|
|
|
|
|
|
|
return field
|
|
|
|
|
|
|
|
|
|
|
2023-11-08 09:18:15 +00:00
|
|
|
class AlertGroupLabelSerializer(serializers.Serializer):
|
|
|
|
|
class KeySerializer(serializers.Serializer):
|
|
|
|
|
id = serializers.CharField(source="key_name")
|
|
|
|
|
name = serializers.CharField(source="key_name")
|
|
|
|
|
|
|
|
|
|
class ValueSerializer(serializers.Serializer):
|
|
|
|
|
id = serializers.CharField(source="value_name")
|
|
|
|
|
name = serializers.CharField(source="value_name")
|
|
|
|
|
|
|
|
|
|
key = KeySerializer(source="*")
|
|
|
|
|
value = ValueSerializer(source="*")
|
|
|
|
|
|
|
|
|
|
|
2023-02-02 11:37:52 +08:00
|
|
|
class ShortAlertGroupSerializer(AlertGroupFieldsCacheSerializerMixin, serializers.ModelSerializer):
|
2022-06-03 08:09:47 -06:00
|
|
|
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"]
|
2023-08-16 14:13:56 +08:00
|
|
|
read_only_fields = ["pk", "render_for_web", "alert_receive_channel", "inside_organization_number"]
|
|
|
|
|
|
2024-01-12 15:11:22 +00:00
|
|
|
def get_render_for_web(self, obj: "AlertGroup") -> RenderForWeb | EmptyRenderForWeb:
|
2023-02-03 19:53:35 +08:00
|
|
|
last_alert = obj.alerts.last()
|
|
|
|
|
if last_alert is None:
|
2023-02-02 11:37:52 +08:00
|
|
|
return {}
|
|
|
|
|
return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
|
|
|
|
|
obj,
|
2023-02-03 19:53:35 +08:00
|
|
|
last_alert,
|
2023-04-14 09:15:57 +02:00
|
|
|
AlertGroupFieldsCacheSerializerMixin.RENDER_FOR_WEB_FIELD_NAME,
|
2023-02-02 11:37:52 +08:00
|
|
|
AlertGroupWebRenderer,
|
|
|
|
|
)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
2023-11-16 07:15:05 -05:00
|
|
|
class AlertGroupListSerializer(
|
|
|
|
|
EagerLoadingMixin, AlertGroupFieldsCacheSerializerMixin, serializers.ModelSerializer[AlertGroup]
|
|
|
|
|
):
|
2022-06-03 08:09:47 -06:00
|
|
|
pk = serializers.CharField(read_only=True, source="public_primary_key")
|
|
|
|
|
alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
|
2022-07-27 12:14:59 +01:00
|
|
|
status = serializers.ReadOnlyField()
|
2022-06-03 08:09:47 -06:00
|
|
|
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()
|
2023-03-22 00:57:20 +08:00
|
|
|
team = TeamPrimaryKeyRelatedField(source="channel.team", allow_null=True)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2022-07-27 12:14:59 +01:00
|
|
|
alerts_count = serializers.IntegerField(read_only=True)
|
2022-06-03 08:09:47 -06:00
|
|
|
render_for_web = serializers.SerializerMethodField()
|
|
|
|
|
|
2023-11-08 09:18:15 +00:00
|
|
|
labels = AlertGroupLabelSerializer(many=True, read_only=True)
|
|
|
|
|
|
2024-10-03 01:09:50 +08:00
|
|
|
PREFETCH_RELATED = [
|
2022-06-03 08:09:47 -06:00
|
|
|
"dependent_alert_groups",
|
|
|
|
|
"log_records__author",
|
2023-11-23 14:28:00 -03:00
|
|
|
"labels",
|
2024-10-03 01:09:50 +08:00
|
|
|
Prefetch(
|
|
|
|
|
"slack_messages",
|
|
|
|
|
queryset=SlackMessage.objects.select_related("_slack_team_identity").order_by("created_at")[:1],
|
|
|
|
|
to_attr="prefetched_slack_messages",
|
|
|
|
|
),
|
|
|
|
|
Prefetch(
|
|
|
|
|
"telegram_messages",
|
|
|
|
|
queryset=TelegramMessage.objects.filter(
|
|
|
|
|
chat_id__startswith="-", message_type=TelegramMessage.ALERT_GROUP_MESSAGE
|
|
|
|
|
).order_by("id")[:1],
|
|
|
|
|
to_attr="prefetched_telegram_messages",
|
|
|
|
|
),
|
2022-06-03 08:09:47 -06:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
SELECT_RELATED = [
|
|
|
|
|
"channel__organization",
|
2023-04-20 18:30:49 +02:00
|
|
|
"channel__team",
|
2022-07-27 12:14:59 +01:00
|
|
|
"root_alert_group",
|
2022-07-21 15:23:02 +01:00
|
|
|
"resolved_by_user",
|
2022-07-27 12:14:59 +01:00
|
|
|
"acknowledged_by_user",
|
2022-06-03 08:09:47 -06:00
|
|
|
"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",
|
|
|
|
|
"dependent_alert_groups",
|
|
|
|
|
"root_alert_group",
|
|
|
|
|
"status",
|
2023-01-17 13:04:50 +01:00
|
|
|
"declare_incident_link",
|
2023-03-22 00:57:20 +08:00
|
|
|
"team",
|
2023-11-01 17:19:44 -04:00
|
|
|
"grafana_incident_id",
|
2023-11-08 09:18:15 +00:00
|
|
|
"labels",
|
2024-03-22 20:31:44 +08:00
|
|
|
"permalinks",
|
2022-06-03 08:09:47 -06:00
|
|
|
]
|
|
|
|
|
|
2024-01-12 15:11:22 +00:00
|
|
|
def get_render_for_web(self, obj: "AlertGroup") -> RenderForWeb | EmptyRenderForWeb:
|
2022-07-27 12:14:59 +01:00
|
|
|
if not obj.last_alert:
|
|
|
|
|
return {}
|
2023-02-02 11:37:52 +08:00
|
|
|
return AlertGroupFieldsCacheSerializerMixin.get_or_set_web_template_field(
|
|
|
|
|
obj,
|
2023-02-03 19:53:35 +08:00
|
|
|
obj.last_alert,
|
2023-04-14 09:15:57 +02:00
|
|
|
AlertGroupFieldsCacheSerializerMixin.RENDER_FOR_WEB_FIELD_NAME,
|
2023-02-02 11:37:52 +08:00
|
|
|
AlertGroupWebRenderer,
|
|
|
|
|
)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-08-16 14:13:56 +08:00
|
|
|
@extend_schema_field(UserShortSerializer(many=True))
|
2023-11-16 07:15:05 -05:00
|
|
|
def get_related_users(self, obj: "AlertGroup"):
|
|
|
|
|
from apps.user_management.models import User
|
|
|
|
|
|
|
|
|
|
users_ids: typing.Set[str] = set()
|
|
|
|
|
users: typing.List[User] = []
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
# 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)
|
2023-08-16 14:13:56 +08:00
|
|
|
users.append(obj.resolved_by_user)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
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)
|
2023-08-16 14:13:56 +08:00
|
|
|
users.append(obj.acknowledged_by_user)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
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)
|
2023-08-16 14:13:56 +08:00
|
|
|
users.append(obj.silenced_by_user)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
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:
|
2023-08-16 14:13:56 +08:00
|
|
|
users.append(log_record.author)
|
2022-06-03 08:09:47 -06:00
|
|
|
users_ids.add(log_record.author.public_primary_key)
|
2024-08-15 16:20:55 +02:00
|
|
|
return UserShortSerializer(users, context=self.context, many=True).data
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2022-07-27 12:14:59 +01:00
|
|
|
|
|
|
|
|
class AlertGroupSerializer(AlertGroupListSerializer):
|
|
|
|
|
alerts = serializers.SerializerMethodField("get_limited_alerts")
|
|
|
|
|
last_alert_at = serializers.SerializerMethodField()
|
2023-01-17 12:19:08 -03:00
|
|
|
paged_users = serializers.SerializerMethodField()
|
2024-04-15 14:49:51 -03:00
|
|
|
external_urls = serializers.SerializerMethodField()
|
2022-07-27 12:14:59 +01:00
|
|
|
|
|
|
|
|
class Meta(AlertGroupListSerializer.Meta):
|
|
|
|
|
fields = AlertGroupListSerializer.Meta.fields + [
|
|
|
|
|
"alerts",
|
|
|
|
|
"render_after_resolve_report_json",
|
2022-11-28 15:52:31 +00:00
|
|
|
"slack_permalink", # TODO: make plugin frontend use "permalinks" field to get Slack link
|
2022-07-27 12:14:59 +01:00
|
|
|
"last_alert_at",
|
2023-01-17 12:19:08 -03:00
|
|
|
"paged_users",
|
2024-04-15 14:49:51 -03:00
|
|
|
"external_urls",
|
2022-07-27 12:14:59 +01:00
|
|
|
]
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-11-16 07:15:05 -05:00
|
|
|
def get_last_alert_at(self, obj: "AlertGroup") -> datetime.datetime:
|
2022-07-27 12:14:59 +01:00
|
|
|
last_alert = obj.alerts.last()
|
|
|
|
|
|
|
|
|
|
if not last_alert:
|
|
|
|
|
return obj.started_at
|
|
|
|
|
|
|
|
|
|
return last_alert.created_at
|
|
|
|
|
|
2023-08-16 14:13:56 +08:00
|
|
|
@extend_schema_field(AlertSerializer(many=True))
|
2023-11-16 07:15:05 -05:00
|
|
|
def get_limited_alerts(self, obj: "AlertGroup"):
|
2022-07-27 12:14:59 +01:00
|
|
|
"""
|
|
|
|
|
Overriding default alerts because there are alert_groups with thousands of them.
|
|
|
|
|
It's just too slow, we need to cut here.
|
|
|
|
|
"""
|
2023-02-28 14:12:56 +00:00
|
|
|
alerts = obj.alerts.order_by("-pk")[:100]
|
2022-07-27 12:14:59 +01:00
|
|
|
return AlertSerializer(alerts, many=True).data
|
2023-01-17 12:19:08 -03:00
|
|
|
|
2023-11-16 07:15:05 -05:00
|
|
|
def get_paged_users(self, obj: "AlertGroup") -> typing.List[PagedUser]:
|
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()
|
2024-04-15 14:49:51 -03:00
|
|
|
|
|
|
|
|
def get_external_urls(self, obj: "AlertGroup") -> typing.List[ExternalURL]:
|
|
|
|
|
external_urls = []
|
|
|
|
|
external_ids = obj.external_ids.all()
|
|
|
|
|
for external_id in external_ids:
|
|
|
|
|
source_integration = external_id.source_alert_receive_channel
|
|
|
|
|
get_url = getattr(source_integration.config, "get_url", None)
|
|
|
|
|
if get_url:
|
|
|
|
|
url = source_integration.config.get_url(source_integration, external_id.value)
|
|
|
|
|
external_urls.append(
|
|
|
|
|
{
|
|
|
|
|
"integration": source_integration.public_primary_key,
|
|
|
|
|
"integration_type": source_integration.integration,
|
|
|
|
|
"external_id": external_id.value,
|
|
|
|
|
"url": url,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return external_urls
|