2022-07-27 12:14:59 +01:00
from datetime import timedelta
2022-06-03 08:09:47 -06:00
2023-01-22 00:14:48 +08:00
from django . core . exceptions import ObjectDoesNotExist
2022-07-27 12:14:59 +01:00
from django . db . models import Count , Max , Q
2022-06-03 08:09:47 -06:00
from django . utils import timezone
from django_filters import rest_framework as filters
from django_filters . widgets import RangeWidget
2023-08-16 14:13:56 +08:00
from drf_spectacular . utils import extend_schema , inline_serializer
from rest_framework import mixins , serializers , status , viewsets
2022-06-03 08:09:47 -06:00
from rest_framework . decorators import action
2023-01-22 00:14:48 +08:00
from rest_framework . exceptions import NotFound
2022-06-03 08:09:47 -06:00
from rest_framework . filters import SearchFilter
from rest_framework . permissions import IsAuthenticated
from rest_framework . response import Response
from apps . alerts . constants import ActionSource
2023-06-07 20:19:16 +08:00
from apps . alerts . models import Alert , AlertGroup , AlertReceiveChannel , EscalationChain , ResolutionNote
2023-01-17 12:19:08 -03:00
from apps . alerts . paging import unpage_user
2023-06-07 20:19:16 +08:00
from apps . alerts . tasks import send_update_resolution_note_signal
from apps . api . errors import AlertGroupAPIError
2022-11-29 09:41:56 +01:00
from apps . api . permissions import RBACPermission
2022-07-27 12:14:59 +01:00
from apps . api . serializers . alert_group import AlertGroupListSerializer , AlertGroupSerializer
2023-01-22 00:14:48 +08:00
from apps . api . serializers . team import TeamSerializer
2022-11-23 15:56:43 +00:00
from apps . auth_token . auth import PluginAuthentication
2023-04-27 11:20:45 +08:00
from apps . base . models . user_notification_policy_log_record import UserNotificationPolicyLogRecord
2022-11-23 15:56:43 +00:00
from apps . mobile_app . auth import MobileAppAuthTokenAuthentication
2023-01-22 00:14:48 +08:00
from apps . user_management . models import Team , User
2022-06-03 08:09:47 -06:00
from common . api_helpers . exceptions import BadRequest
2023-03-22 00:57:20 +08:00
from common . api_helpers . filters import (
ByTeamModelFieldFilterMixin ,
DateRangeFilterMixin ,
ModelFieldFilterMixin ,
TeamModelMultipleChoiceFilter ,
)
2022-08-17 08:46:53 +01:00
from common . api_helpers . mixins import PreviewTemplateMixin , PublicPrimaryKeyMixin , TeamFilteringMixin
2022-07-27 12:14:59 +01:00
from common . api_helpers . paginators import TwentyFiveCursorPaginator
2022-06-03 08:09:47 -06:00
def get_integration_queryset ( request ) :
if request is None :
return AlertReceiveChannel . objects . none ( )
return AlertReceiveChannel . objects_with_maintenance . filter ( organization = request . user . organization )
2023-03-14 14:38:18 +00:00
def get_escalation_chain_queryset ( request ) :
if request is None :
return EscalationChain . objects . none ( )
return EscalationChain . objects . filter ( organization = request . user . organization )
2022-06-03 08:09:47 -06:00
def get_user_queryset ( request ) :
if request is None :
return User . objects . none ( )
return User . objects . filter ( organization = request . user . organization ) . distinct ( )
2023-04-26 02:16:29 -04:00
class AlertGroupFilterBackend ( filters . DjangoFilterBackend ) :
"""
See here for more context on how this works
https : / / github . com / carltongibson / django - filter / discussions / 1572
https : / / youtu . be / e52S1SjuUeM ? t = 841
"""
def get_filterset ( self , request , queryset , view ) :
filterset = super ( ) . get_filterset ( request , queryset , view )
filterset . form . fields [ " integration " ] . queryset = get_integration_queryset ( request )
filterset . form . fields [ " escalation_chain " ] . queryset = get_escalation_chain_queryset ( request )
user_queryset = get_user_queryset ( request )
filterset . form . fields [ " silenced_by " ] . queryset = user_queryset
filterset . form . fields [ " acknowledged_by " ] . queryset = user_queryset
filterset . form . fields [ " resolved_by " ] . queryset = user_queryset
filterset . form . fields [ " invitees_are " ] . queryset = user_queryset
filterset . form . fields [ " involved_users_are " ] . queryset = user_queryset
return filterset
2023-03-22 00:57:20 +08:00
class AlertGroupFilter ( DateRangeFilterMixin , ByTeamModelFieldFilterMixin , ModelFieldFilterMixin , filters . FilterSet ) :
2022-06-03 08:09:47 -06:00
"""
Examples of possible date formats here https : / / docs . djangoproject . com / en / 1.9 / ref / settings / #datetime-input-formats
"""
2023-05-02 13:50:03 +08:00
FILTER_BY_INVOLVED_USERS_ALERT_GROUPS_CUTOFF = 1000
2022-06-03 08:09:47 -06:00
started_at_gte = filters . DateTimeFilter ( field_name = " started_at " , lookup_expr = " gte " )
started_at_lte = filters . DateTimeFilter ( field_name = " started_at " , lookup_expr = " lte " )
resolved_at_lte = filters . DateTimeFilter ( field_name = " resolved_at " , lookup_expr = " lte " )
is_root = filters . BooleanFilter ( field_name = " root_alert_group " , lookup_expr = " isnull " )
id__in = filters . BaseInFilter ( field_name = " public_primary_key " , lookup_expr = " in " )
status = filters . MultipleChoiceFilter ( choices = AlertGroup . STATUS_CHOICES , method = " filter_status " )
started_at = filters . CharFilter ( field_name = " started_at " , method = DateRangeFilterMixin . filter_date_range . __name__ )
resolved_at = filters . CharFilter ( field_name = " resolved_at " , method = DateRangeFilterMixin . filter_date_range . __name__ )
silenced_at = filters . CharFilter ( field_name = " silenced_at " , method = DateRangeFilterMixin . filter_date_range . __name__ )
silenced_by = filters . ModelMultipleChoiceFilter (
field_name = " silenced_by_user " ,
2023-04-26 02:16:29 -04:00
queryset = None ,
2022-06-03 08:09:47 -06:00
to_field_name = " public_primary_key " ,
method = ModelFieldFilterMixin . filter_model_field . __name__ ,
)
integration = filters . ModelMultipleChoiceFilter (
2023-06-08 16:49:48 -03:00
field_name = " channel " ,
2023-04-26 02:16:29 -04:00
queryset = None ,
2022-06-03 08:09:47 -06:00
to_field_name = " public_primary_key " ,
method = ModelFieldFilterMixin . filter_model_field . __name__ ,
)
2023-03-14 14:38:18 +00:00
escalation_chain = filters . ModelMultipleChoiceFilter (
field_name = " channel_filter__escalation_chain " ,
2023-04-26 02:16:29 -04:00
queryset = None ,
2023-03-14 14:38:18 +00:00
to_field_name = " public_primary_key " ,
method = ModelFieldFilterMixin . filter_model_field . __name__ ,
)
2022-06-03 08:09:47 -06:00
started_at_range = filters . DateFromToRangeFilter (
field_name = " started_at " , widget = RangeWidget ( attrs = { " type " : " date " } )
)
resolved_by = filters . ModelMultipleChoiceFilter (
field_name = " resolved_by_user " ,
2023-04-26 02:16:29 -04:00
queryset = None ,
2022-06-03 08:09:47 -06:00
to_field_name = " public_primary_key " ,
method = ModelFieldFilterMixin . filter_model_field . __name__ ,
)
acknowledged_by = filters . ModelMultipleChoiceFilter (
field_name = " acknowledged_by_user " ,
2023-04-26 02:16:29 -04:00
queryset = None ,
2022-06-03 08:09:47 -06:00
to_field_name = " public_primary_key " ,
method = ModelFieldFilterMixin . filter_model_field . __name__ ,
)
invitees_are = filters . ModelMultipleChoiceFilter (
2023-04-26 02:16:29 -04:00
queryset = None , to_field_name = " public_primary_key " , method = " filter_invitees_are "
2022-06-03 08:09:47 -06:00
)
2023-01-30 09:08:18 -03:00
involved_users_are = filters . ModelMultipleChoiceFilter (
2023-04-26 02:16:29 -04:00
queryset = None , to_field_name = " public_primary_key " , method = " filter_by_involved_users "
2023-01-30 09:08:18 -03:00
)
2022-06-03 08:09:47 -06:00
with_resolution_note = filters . BooleanFilter ( method = " filter_with_resolution_note " )
2023-01-30 09:08:18 -03:00
mine = filters . BooleanFilter ( method = " filter_mine " )
2023-03-22 00:57:20 +08:00
team = TeamModelMultipleChoiceFilter ( field_name = " channel__team " )
2022-06-03 08:09:47 -06:00
class Meta :
model = AlertGroup
fields = [
" id__in " ,
" started_at_gte " ,
" started_at_lte " ,
" resolved_at_lte " ,
" is_root " ,
" resolved_by " ,
" acknowledged_by " ,
]
def filter_status ( self , queryset , name , value ) :
if not value :
return queryset
try :
statuses = list ( map ( int , value ) )
except ValueError :
raise BadRequest ( detail = " Invalid status value " )
filters = { }
q_objects = Q ( )
if AlertGroup . NEW in statuses :
2023-01-17 23:28:29 +13:00
filters [ " new " ] = AlertGroup . get_new_state_filter ( )
2022-06-03 08:09:47 -06:00
if AlertGroup . SILENCED in statuses :
2023-01-17 23:28:29 +13:00
filters [ " silenced " ] = AlertGroup . get_silenced_state_filter ( )
2022-06-03 08:09:47 -06:00
if AlertGroup . ACKNOWLEDGED in statuses :
2023-01-17 23:28:29 +13:00
filters [ " acknowledged " ] = AlertGroup . get_acknowledged_state_filter ( )
2022-06-03 08:09:47 -06:00
if AlertGroup . RESOLVED in statuses :
2023-01-17 23:28:29 +13:00
filters [ " resolved " ] = AlertGroup . get_resolved_state_filter ( )
2022-06-03 08:09:47 -06:00
for item in filters :
q_objects | = filters [ item ]
queryset = queryset . filter ( q_objects )
return queryset
def filter_invitees_are ( self , queryset , name , value ) :
users = value
if not users :
return queryset
2023-01-30 09:08:18 -03:00
queryset = queryset . filter ( log_records__author__in = users ) . distinct ( )
return queryset
def filter_by_involved_users ( self , queryset , name , value ) :
users = value
if not users :
return queryset
2023-04-27 11:20:45 +08:00
# This is expensive to filter all alert groups with involved users,
# so we limit the number of alert groups to filter by the last 1000 for the given user(s)
alert_group_notified_users_ids = list (
UserNotificationPolicyLogRecord . objects . filter ( author__in = users )
. order_by ( " -alert_group_id " )
. values_list ( " alert_group_id " , flat = True )
2023-05-02 13:50:03 +08:00
. distinct ( ) [ : self . FILTER_BY_INVOLVED_USERS_ALERT_GROUPS_CUTOFF ]
2023-04-27 11:20:45 +08:00
)
2023-01-30 09:08:18 -03:00
queryset = queryset . filter (
2023-03-07 08:38:50 -03:00
# user was notified
2023-04-27 11:20:45 +08:00
Q ( id__in = alert_group_notified_users_ids )
2023-03-07 08:38:50 -03:00
|
# or interacted with the alert group
2023-03-08 12:27:03 -03:00
Q ( acknowledged_by_user__in = users )
| Q ( resolved_by_user__in = users )
| Q ( silenced_by_user__in = users )
2023-01-30 09:08:18 -03:00
) . distinct ( )
return queryset
2022-06-03 08:09:47 -06:00
2023-01-30 09:08:18 -03:00
def filter_mine ( self , queryset , name , value ) :
if value :
2023-03-08 12:27:03 -03:00
return self . filter_by_involved_users ( queryset , " users " , [ self . request . user ] )
2022-06-03 08:09:47 -06:00
return queryset
def filter_with_resolution_note ( self , queryset , name , value ) :
if value is True :
queryset = queryset . filter ( Q ( resolution_notes__isnull = False , resolution_notes__deleted_at = None ) ) . distinct ( )
elif value is False :
queryset = queryset . filter (
Q ( resolution_notes__isnull = True ) | ~ Q ( resolution_notes__deleted_at = None )
) . distinct ( )
return queryset
2022-08-17 08:46:53 +01:00
class AlertGroupTeamFilteringMixin ( TeamFilteringMixin ) :
2023-04-20 21:01:41 +08:00
TEAM_LOOKUP = " team "
2022-08-17 08:46:53 +01:00
2023-01-22 00:14:48 +08:00
def retrieve ( self , request , * args , * * kwargs ) :
try :
return super ( ) . retrieve ( request , * args , * * kwargs )
except NotFound :
Optimize alertgroups endpoint (#1189)
# What this PR does
Changing query to retrieve alert group in two completely different
queries instead of one with `join`
new queries
```
SELECT alerts_alertreceivechannel.id
FROM alerts_alertreceivechannel
WHERE (alerts_alertreceivechannel.deleted_at IS NULL
AND alerts_alertreceivechannel.organization_id = 8
AND alerts_alertreceivechannel.team_id IS NULL)
SELECT `alerts_alertgroup`.`id`
FROM `alerts_alertgroup`
WHERE (`alerts_alertgroup`.`channel_id` IN (2,33,34,35,36,40,52,59,61,62,63,70,76,89,93,94,03,08,09,10,12,13,16,18,20,22,23,24,26,27,28,30,31,33,34,35,36,40,41,42,43,45,48,53,56,57,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,91,93,23,27,29,31,32,33,55,56,57,58,65,69,72,75,81,13,17,20,22,33,34,38,39,41,44,45,46,51,52,55,56,58,59,60,63,68,70,71)
AND NOT `alerts_alertgroup`.`is_archived`
AND NOT `alerts_alertgroup`.`is_archived`
AND `alerts_alertgroup`.`root_alert_group_id` IS NULL
AND ((NOT `alerts_alertgroup`.`silenced`
AND NOT `alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`)
OR (`alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`))
AND NOT `alerts_alertgroup`.`is_archived`)
ORDER BY `alerts_alertgroup`.`id` DESC
LIMIT 26
```
## Which issue(s) this PR fixes
## Checklist
- [ ] Tests updated
- [ ] Documentation added
- [ ] `CHANGELOG.md` updated
2023-01-22 00:53:11 +08:00
alert_receive_channels_ids = list (
AlertReceiveChannel . objects . filter (
organization_id = self . request . auth . organization . id ,
) . values_list ( " id " , flat = True )
)
2023-07-18 13:48:34 +02:00
queryset = AlertGroup . objects . filter (
Optimize alertgroups endpoint (#1189)
# What this PR does
Changing query to retrieve alert group in two completely different
queries instead of one with `join`
new queries
```
SELECT alerts_alertreceivechannel.id
FROM alerts_alertreceivechannel
WHERE (alerts_alertreceivechannel.deleted_at IS NULL
AND alerts_alertreceivechannel.organization_id = 8
AND alerts_alertreceivechannel.team_id IS NULL)
SELECT `alerts_alertgroup`.`id`
FROM `alerts_alertgroup`
WHERE (`alerts_alertgroup`.`channel_id` IN (2,33,34,35,36,40,52,59,61,62,63,70,76,89,93,94,03,08,09,10,12,13,16,18,20,22,23,24,26,27,28,30,31,33,34,35,36,40,41,42,43,45,48,53,56,57,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,91,93,23,27,29,31,32,33,55,56,57,58,65,69,72,75,81,13,17,20,22,33,34,38,39,41,44,45,46,51,52,55,56,58,59,60,63,68,70,71)
AND NOT `alerts_alertgroup`.`is_archived`
AND NOT `alerts_alertgroup`.`is_archived`
AND `alerts_alertgroup`.`root_alert_group_id` IS NULL
AND ((NOT `alerts_alertgroup`.`silenced`
AND NOT `alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`)
OR (`alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`))
AND NOT `alerts_alertgroup`.`is_archived`)
ORDER BY `alerts_alertgroup`.`id` DESC
LIMIT 26
```
## Which issue(s) this PR fixes
## Checklist
- [ ] Tests updated
- [ ] Documentation added
- [ ] `CHANGELOG.md` updated
2023-01-22 00:53:11 +08:00
channel__in = alert_receive_channels_ids ,
2023-01-22 00:14:48 +08:00
) . only ( " public_primary_key " )
try :
obj = queryset . get ( public_primary_key = self . kwargs [ " pk " ] )
except ObjectDoesNotExist :
raise NotFound
obj_team = self . _getattr_with_related ( obj , self . TEAM_LOOKUP )
if obj_team is None or obj_team in self . request . user . teams . all ( ) :
if obj_team is None :
obj_team = Team ( public_primary_key = None , name = " General " , email = None , avatar_url = None )
return Response (
data = { " error_code " : " wrong_team " , " owner_team " : TeamSerializer ( obj_team ) . data } ,
status = status . HTTP_403_FORBIDDEN ,
)
return Response ( data = { " error_code " : " wrong_team " } , status = status . HTTP_403_FORBIDDEN )
2022-08-17 08:46:53 +01:00
2022-06-03 08:09:47 -06:00
class AlertGroupView (
PreviewTemplateMixin ,
2022-08-17 08:46:53 +01:00
AlertGroupTeamFilteringMixin ,
2022-06-03 08:09:47 -06:00
PublicPrimaryKeyMixin ,
mixins . RetrieveModelMixin ,
mixins . ListModelMixin ,
viewsets . GenericViewSet ,
) :
authentication_classes = (
MobileAppAuthTokenAuthentication ,
PluginAuthentication ,
)
2022-11-29 09:41:56 +01:00
permission_classes = ( IsAuthenticated , RBACPermission )
rbac_permissions = {
" metadata " : [ RBACPermission . Permissions . ALERT_GROUPS_READ ] ,
" list " : [ RBACPermission . Permissions . ALERT_GROUPS_READ ] ,
" retrieve " : [ RBACPermission . Permissions . ALERT_GROUPS_READ ] ,
" stats " : [ RBACPermission . Permissions . ALERT_GROUPS_READ ] ,
" filters " : [ RBACPermission . Permissions . ALERT_GROUPS_READ ] ,
" silence_options " : [ RBACPermission . Permissions . ALERT_GROUPS_READ ] ,
" bulk_action_options " : [ RBACPermission . Permissions . ALERT_GROUPS_READ ] ,
" create " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" update " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" destroy " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" acknowledge " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" unacknowledge " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" resolve " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" unresolve " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" attach " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" unattach " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" silence " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" unsilence " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
2023-01-17 12:19:08 -03:00
" unpage_user " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
2022-11-29 09:41:56 +01:00
" bulk_action " : [ RBACPermission . Permissions . ALERT_GROUPS_WRITE ] ,
" preview_template " : [ RBACPermission . Permissions . INTEGRATIONS_TEST ] ,
2022-06-03 08:09:47 -06:00
}
http_method_names = [ " get " , " post " ]
serializer_class = AlertGroupSerializer
2022-07-27 12:14:59 +01:00
pagination_class = TwentyFiveCursorPaginator
2022-06-03 08:09:47 -06:00
2023-04-26 02:16:29 -04:00
filter_backends = [ SearchFilter , AlertGroupFilterBackend ]
2023-06-07 07:14:21 +03:00
# search_fields = ["=public_primary_key", "=inside_organization_number", "web_title_cache"]
2022-06-03 08:09:47 -06:00
filterset_class = AlertGroupFilter
2022-07-27 12:14:59 +01:00
def get_serializer_class ( self ) :
if self . action == " list " :
return AlertGroupListSerializer
return super ( ) . get_serializer_class ( )
2023-03-22 00:57:20 +08:00
def get_queryset ( self , ignore_filtering_by_available_teams = False ) :
2022-07-27 12:14:59 +01:00
# no select_related or prefetch_related is used at this point, it will be done on paginate_queryset.
2023-04-20 21:01:41 +08:00
2023-11-02 16:45:30 +08:00
alert_receive_channels_qs = AlertReceiveChannel . objects_with_deleted . filter (
2023-04-20 21:01:41 +08:00
organization_id = self . request . auth . organization . id
Optimize alertgroups endpoint (#1189)
# What this PR does
Changing query to retrieve alert group in two completely different
queries instead of one with `join`
new queries
```
SELECT alerts_alertreceivechannel.id
FROM alerts_alertreceivechannel
WHERE (alerts_alertreceivechannel.deleted_at IS NULL
AND alerts_alertreceivechannel.organization_id = 8
AND alerts_alertreceivechannel.team_id IS NULL)
SELECT `alerts_alertgroup`.`id`
FROM `alerts_alertgroup`
WHERE (`alerts_alertgroup`.`channel_id` IN (2,33,34,35,36,40,52,59,61,62,63,70,76,89,93,94,03,08,09,10,12,13,16,18,20,22,23,24,26,27,28,30,31,33,34,35,36,40,41,42,43,45,48,53,56,57,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,91,93,23,27,29,31,32,33,55,56,57,58,65,69,72,75,81,13,17,20,22,33,34,38,39,41,44,45,46,51,52,55,56,58,59,60,63,68,70,71)
AND NOT `alerts_alertgroup`.`is_archived`
AND NOT `alerts_alertgroup`.`is_archived`
AND `alerts_alertgroup`.`root_alert_group_id` IS NULL
AND ((NOT `alerts_alertgroup`.`silenced`
AND NOT `alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`)
OR (`alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`))
AND NOT `alerts_alertgroup`.`is_archived`)
ORDER BY `alerts_alertgroup`.`id` DESC
LIMIT 26
```
## Which issue(s) this PR fixes
## Checklist
- [ ] Tests updated
- [ ] Documentation added
- [ ] `CHANGELOG.md` updated
2023-01-22 00:53:11 +08:00
)
2023-04-20 21:01:41 +08:00
if not ignore_filtering_by_available_teams :
alert_receive_channels_qs = alert_receive_channels_qs . filter ( * self . available_teams_lookup_args )
alert_receive_channels_ids = list ( alert_receive_channels_qs . values_list ( " id " , flat = True ) )
2023-03-22 00:57:20 +08:00
2023-07-18 13:48:34 +02:00
queryset = AlertGroup . objects . filter (
Optimize alertgroups endpoint (#1189)
# What this PR does
Changing query to retrieve alert group in two completely different
queries instead of one with `join`
new queries
```
SELECT alerts_alertreceivechannel.id
FROM alerts_alertreceivechannel
WHERE (alerts_alertreceivechannel.deleted_at IS NULL
AND alerts_alertreceivechannel.organization_id = 8
AND alerts_alertreceivechannel.team_id IS NULL)
SELECT `alerts_alertgroup`.`id`
FROM `alerts_alertgroup`
WHERE (`alerts_alertgroup`.`channel_id` IN (2,33,34,35,36,40,52,59,61,62,63,70,76,89,93,94,03,08,09,10,12,13,16,18,20,22,23,24,26,27,28,30,31,33,34,35,36,40,41,42,43,45,48,53,56,57,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,91,93,23,27,29,31,32,33,55,56,57,58,65,69,72,75,81,13,17,20,22,33,34,38,39,41,44,45,46,51,52,55,56,58,59,60,63,68,70,71)
AND NOT `alerts_alertgroup`.`is_archived`
AND NOT `alerts_alertgroup`.`is_archived`
AND `alerts_alertgroup`.`root_alert_group_id` IS NULL
AND ((NOT `alerts_alertgroup`.`silenced`
AND NOT `alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`)
OR (`alerts_alertgroup`.`acknowledged`
AND NOT `alerts_alertgroup`.`resolved`))
AND NOT `alerts_alertgroup`.`is_archived`)
ORDER BY `alerts_alertgroup`.`id` DESC
LIMIT 26
```
## Which issue(s) this PR fixes
## Checklist
- [ ] Tests updated
- [ ] Documentation added
- [ ] `CHANGELOG.md` updated
2023-01-22 00:53:11 +08:00
channel__in = alert_receive_channels_ids ,
2023-03-22 00:57:20 +08:00
)
queryset = queryset . only ( " id " )
2022-07-27 12:14:59 +01:00
return queryset
def paginate_queryset ( self , queryset ) :
2022-06-03 08:09:47 -06:00
"""
2022-07-27 12:14:59 +01:00
All SQL joins ( select_related and prefetch_related ) will be performed AFTER pagination , so it only joins tables
for 25 alert groups , not the whole table .
2022-07-14 15:19:25 +01:00
"""
2022-07-27 12:14:59 +01:00
alert_groups = super ( ) . paginate_queryset ( queryset )
alert_groups = self . enrich ( alert_groups )
return alert_groups
2022-06-03 08:09:47 -06:00
2022-07-27 12:14:59 +01:00
def get_object ( self ) :
obj = super ( ) . get_object ( )
obj = self . enrich ( [ obj ] ) [ 0 ]
return obj
2022-06-03 08:09:47 -06:00
2023-09-11 10:25:00 -03:00
def retrieve ( self , request , pk , * args , * * kwargs ) :
""" Return alert group details.
It is worth mentioning that ` render_after_resolve_report_json ` property will return a list
of log entries including actions involving the alert group , notifications triggered for a user
and resolution notes updates .
A few additional notes about the possible values for each key in the logs :
- ` time ` : humanized time delta respect to now when the action took place
- ` action ` : human - readable description of the action
- ` realm ` : resource involved in the action ; one of three possible values :
` alert_group ` , ` user_notification ` , ` resolution_note `
- ` type ` : integer value indicating the type of action ( see below )
- ` created_at ` : timestamp corresponding to when the action happened
- ` author ` : details about the user performing the action
Possible ` type ` values depending on the realm value :
For ` alert_group ` :
- 0 : Acknowledged
- 1 : Unacknowledged
- 2 : Invite
- 3 : Stop invitation
- 4 : Re - invite
- 5 : Escalation triggered
- 6 : Invitation triggered
- 7 : Silenced
- 8 : Attached
- 9 : Unattached
- 10 : Custom button triggered
- 11 : Unacknowledged by timeout
- 12 : Failed attachment
- 13 : Incident resolved
- 14 : Incident unresolved
- 15 : Unsilenced
- 16 : Escalation finished
- 17 : Escalation failed
- 18 : Acknowledge reminder triggered
- 19 : Wiped
- 20 : Deleted
- 21 : Incident registered
- 22 : A route is assigned to the incident
- 23 : Trigger direct paging escalation
- 24 : Unpage a user
- 25 : Restricted
For ` user_notification ` :
- 0 : Personal notification triggered
- 1 : Personal notification finished
- 2 : Personal notification success ,
- 3 : Personal notification failed
For ` resolution_note ` :
- 0 : slack
- 1 : web
"""
return super ( ) . retrieve ( request , pk , * args , * * kwargs )
2022-07-27 12:14:59 +01:00
def enrich ( self , alert_groups ) :
"""
This method performs select_related and prefetch_related ( using setup_eager_loading ) as well as in - memory joins
to add additional info like alert_count and last_alert for every alert group efficiently .
We need the last_alert because it ' s used by AlertGroupWebRenderer.
"""
2022-06-03 08:09:47 -06:00
2022-07-27 12:14:59 +01:00
# enrich alert groups with select_related and prefetch_related
alert_group_pks = [ alert_group . pk for alert_group in alert_groups ]
2023-11-01 13:19:21 -06:00
queryset = AlertGroup . objects . filter ( pk__in = alert_group_pks ) . order_by ( " -started_at " )
2022-06-03 08:09:47 -06:00
2022-07-27 12:14:59 +01:00
queryset = self . get_serializer_class ( ) . setup_eager_loading ( queryset )
alert_groups = list ( queryset )
# get info on alerts count and last alert ID for every alert group
alerts_info = (
Alert . objects . values ( " group_id " )
. filter ( group_id__in = alert_group_pks )
. annotate ( alerts_count = Count ( " group_id " ) , last_alert_id = Max ( " id " ) )
)
alerts_info_map = { info [ " group_id " ] : info for info in alerts_info }
# fetch last alerts for every alert group
last_alert_ids = [ info [ " last_alert_id " ] for info in alerts_info_map . values ( ) ]
last_alerts = Alert . objects . filter ( pk__in = last_alert_ids )
for alert in last_alerts :
# link group back to alert
alert . group = [ alert_group for alert_group in alert_groups if alert_group . pk == alert . group_id ] [ 0 ]
alerts_info_map [ alert . group_id ] . update ( { " last_alert " : alert } )
# add additional "alerts_count" and "last_alert" fields to every alert group
for alert_group in alert_groups :
try :
alert_group . last_alert = alerts_info_map [ alert_group . pk ] [ " last_alert " ]
alert_group . alerts_count = alerts_info_map [ alert_group . pk ] [ " alerts_count " ]
except KeyError :
# alert group has no alerts
alert_group . last_alert = None
alert_group . alerts_count = 0
return alert_groups
2022-06-03 08:09:47 -06:00
2023-08-16 14:13:56 +08:00
@extend_schema ( responses = inline_serializer ( name = " AlertGroupStats " , fields = { " count " : serializers . IntegerField ( ) } ) )
2022-06-03 08:09:47 -06:00
@action ( detail = False )
def stats ( self , * args , * * kwargs ) :
2023-08-16 14:13:56 +08:00
""" Return number of alert groups capped at 100001 """
2023-06-27 10:58:16 +08:00
MAX_COUNT = 100001
alert_groups = self . filter_queryset ( self . get_queryset ( ) ) [ : MAX_COUNT ]
count = alert_groups . count ( )
count = f " { MAX_COUNT - 1 } + " if count == MAX_COUNT else str ( count )
2022-06-03 08:09:47 -06:00
return Response (
{
2023-06-27 10:58:16 +08:00
" count " : count ,
2022-06-03 08:09:47 -06:00
}
)
@action ( methods = [ " post " ] , detail = True )
def acknowledge ( self , request , pk ) :
alert_group = self . get_object ( )
if alert_group . is_maintenance_incident :
raise BadRequest ( detail = " Can ' t acknowledge maintenance alert group " )
if alert_group . root_alert_group is not None :
raise BadRequest ( detail = " Can ' t acknowledge an attached alert group " )
alert_group . acknowledge_by_user ( self . request . user , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : self . request } ) . data )
@action ( methods = [ " post " ] , detail = True )
def unacknowledge ( self , request , pk ) :
alert_group = self . get_object ( )
if alert_group . is_maintenance_incident :
raise BadRequest ( detail = " Can ' t unacknowledge maintenance alert group " )
if alert_group . root_alert_group is not None :
raise BadRequest ( detail = " Can ' t unacknowledge an attached alert group " )
if not alert_group . acknowledged :
raise BadRequest ( detail = " The alert group is not acknowledged " )
if alert_group . resolved :
raise BadRequest ( detail = " Can ' t unacknowledge a resolved alert group " )
alert_group . un_acknowledge_by_user ( self . request . user , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : self . request } ) . data )
@action ( methods = [ " post " ] , detail = True )
def resolve ( self , request , pk ) :
alert_group = self . get_object ( )
organization = self . request . user . organization
if alert_group . root_alert_group is not None :
raise BadRequest ( detail = " Can ' t resolve an attached alert group " )
if alert_group . is_maintenance_incident :
alert_group . stop_maintenance ( self . request . user )
else :
2023-06-07 20:19:16 +08:00
resolution_note_text = request . data . get ( " resolution_note " )
if resolution_note_text :
rn = ResolutionNote . objects . create (
alert_group = alert_group ,
author = self . request . user ,
2023-10-20 15:22:45 +01:00
source = (
ResolutionNote . Source . MOBILE_APP
if isinstance ( self . request . successful_authenticator , MobileAppAuthTokenAuthentication )
else ResolutionNote . Source . WEB
) ,
2023-06-07 20:19:16 +08:00
message_text = resolution_note_text [ : 3000 ] , # trim text to fit in the db field
)
send_update_resolution_note_signal . apply_async (
kwargs = {
" alert_group_pk " : alert_group . pk ,
" resolution_note_pk " : rn . pk ,
}
2022-06-03 08:09:47 -06:00
)
2023-06-07 20:19:16 +08:00
else :
# Check resolution note required setting only if resolution_note_text was not provided.
if organization . is_resolution_note_required and not alert_group . has_resolution_notes :
return Response (
data = {
" code " : AlertGroupAPIError . RESOLUTION_NOTE_REQUIRED . value ,
" detail " : " Alert group without resolution note cannot be resolved due to organization settings " ,
} ,
status = status . HTTP_400_BAD_REQUEST ,
)
2022-06-03 08:09:47 -06:00
alert_group . resolve_by_user ( self . request . user , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : self . request } ) . data )
@action ( methods = [ " post " ] , detail = True )
def unresolve ( self , request , pk ) :
alert_group = self . get_object ( )
if alert_group . is_maintenance_incident :
raise BadRequest ( detail = " Can ' t unresolve maintenance alert group " )
if alert_group . root_alert_group is not None :
raise BadRequest ( detail = " Can ' t unresolve an attached alert group " )
if not alert_group . resolved :
raise BadRequest ( detail = " The alert group is not resolved " )
alert_group . un_resolve_by_user ( self . request . user , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : self . request } ) . data )
@action ( methods = [ " post " ] , detail = True )
def attach ( self , request , pk = None ) :
2023-08-16 14:13:56 +08:00
"""
Attach alert group to another alert group
"""
2022-06-03 08:09:47 -06:00
alert_group = self . get_object ( )
if alert_group . is_maintenance_incident :
raise BadRequest ( detail = " Can ' t attach maintenance alert group " )
if alert_group . dependent_alert_groups . count ( ) > 0 :
raise BadRequest ( detail = " Can ' t attach an alert group because it has another alert groups attached to it " )
if not alert_group . is_root_alert_group :
raise BadRequest ( detail = " Can ' t attach an alert group because it has already been attached " )
try :
root_alert_group = self . get_queryset ( ) . get ( public_primary_key = request . data [ " root_alert_group_pk " ] )
except AlertGroup . DoesNotExist :
return Response ( status = status . HTTP_400_BAD_REQUEST )
if root_alert_group . resolved or root_alert_group . root_alert_group is not None :
return Response ( status = status . HTTP_400_BAD_REQUEST )
if root_alert_group == alert_group :
return Response ( status = status . HTTP_400_BAD_REQUEST )
alert_group . attach_by_user ( self . request . user , root_alert_group , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : self . request } ) . data )
@action ( methods = [ " post " ] , detail = True )
def unattach ( self , request , pk = None ) :
alert_group = self . get_object ( )
if alert_group . is_maintenance_incident :
raise BadRequest ( detail = " Can ' t unattach maintenance alert group " )
if alert_group . is_root_alert_group :
raise BadRequest ( detail = " Can ' t unattach an alert group because it is not attached " )
2022-07-27 12:14:59 +01:00
2022-06-03 08:09:47 -06:00
alert_group . un_attach_by_user ( self . request . user , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : self . request } ) . data )
@action ( methods = [ " post " ] , detail = True )
def silence ( self , request , pk = None ) :
alert_group = self . get_object ( )
delay = request . data . get ( " delay " )
if delay is None :
raise BadRequest ( detail = " Please specify a delay for silence " )
if alert_group . root_alert_group is not None :
raise BadRequest ( detail = " Can ' t silence an attached alert group " )
alert_group . silence_by_user ( request . user , silence_delay = delay , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : request } ) . data )
2023-08-16 14:13:56 +08:00
@extend_schema (
responses = inline_serializer (
name = " silence_options " ,
fields = { " value " : serializers . CharField ( ) , " display_name " : serializers . CharField ( ) } ,
many = True ,
)
)
2022-06-03 08:09:47 -06:00
@action ( methods = [ " get " ] , detail = False )
def silence_options ( self , request ) :
data = [
{ " value " : value , " display_name " : display_name } for value , display_name in AlertGroup . SILENCE_DELAY_OPTIONS
]
return Response ( data )
@action ( methods = [ " post " ] , detail = True )
def unsilence ( self , request , pk = None ) :
alert_group = self . get_object ( )
if not alert_group . silenced :
raise BadRequest ( detail = " The alert group is not silenced " )
if alert_group . resolved :
raise BadRequest ( detail = " Can ' t unsilence a resolved alert group " )
if alert_group . acknowledged :
raise BadRequest ( detail = " Can ' t unsilence an acknowledged alert group " )
if alert_group . root_alert_group is not None :
raise BadRequest ( detail = " Can ' t unsilence an attached alert group " )
alert_group . un_silence_by_user ( request . user , action_source = ActionSource . WEB )
return Response ( AlertGroupSerializer ( alert_group , context = { " request " : request } ) . data )
2023-01-17 12:19:08 -03:00
@action ( methods = [ " post " ] , detail = True )
def unpage_user ( self , request , pk = None ) :
organization = request . auth . organization
from_user = request . user
alert_group = self . get_object ( )
try :
user_id = request . data [ " user_id " ]
except KeyError :
raise BadRequest ( detail = " Please specify user_id " )
try :
user = organization . users . get ( public_primary_key = user_id )
except User . DoesNotExist :
raise BadRequest ( detail = " User not found " )
unpage_user ( alert_group = alert_group , user = user , from_user = from_user )
return Response ( status = status . HTTP_200_OK )
2022-06-03 08:09:47 -06:00
@action ( methods = [ " get " ] , detail = False )
def filters ( self , request ) :
filter_name = request . query_params . get ( " search " , None )
api_root = " /api/internal/v1/ "
now = timezone . now ( )
week_ago = now - timedelta ( days = 7 )
default_datetime_range = " {} / {} " . format (
week_ago . strftime ( DateRangeFilterMixin . DATE_FORMAT ) ,
now . strftime ( DateRangeFilterMixin . DATE_FORMAT ) ,
)
filter_options = [
2023-03-22 00:57:20 +08:00
{
" name " : " team " ,
" type " : " team_select " ,
" href " : api_root + " teams/ " ,
" global " : True ,
} ,
2022-06-03 08:09:47 -06:00
{ " name " : " search " , " type " : " search " } ,
{ " name " : " integration " , " type " : " options " , " href " : api_root + " alert_receive_channels/?filters=true " } ,
2023-03-14 14:38:18 +00:00
{ " name " : " escalation_chain " , " type " : " options " , " href " : api_root + " escalation_chains/?filters=true " } ,
2022-06-03 08:09:47 -06:00
{
" name " : " acknowledged_by " ,
" type " : " options " ,
" href " : api_root + " users/?filters=true&roles=0&roles=1&roles=2 " ,
" default " : { " display_name " : self . request . user . username , " value " : self . request . user . public_primary_key } ,
} ,
{
" name " : " resolved_by " ,
" type " : " options " ,
" href " : api_root + " users/?filters=true&roles=0&roles=1&roles=2 " ,
} ,
{
" name " : " silenced_by " ,
" type " : " options " ,
" href " : api_root + " users/?filters=true&roles=0&roles=1&roles=2 " ,
} ,
{
" name " : " invitees_are " ,
" type " : " options " ,
" href " : api_root + " users/?filters=true&roles=0&roles=1&roles=2 " ,
} ,
2023-01-30 09:08:18 -03:00
{
" name " : " involved_users_are " ,
" type " : " options " ,
" href " : api_root + " users/?filters=true&roles=0&roles=1&roles=2 " ,
" default " : { " display_name " : self . request . user . username , " value " : self . request . user . public_primary_key } ,
2023-05-02 13:50:03 +08:00
" description " : f " This filter works only for last { AlertGroupFilter . FILTER_BY_INVOLVED_USERS_ALERT_GROUPS_CUTOFF } alert groups these users involved in. " ,
2023-01-30 09:08:18 -03:00
} ,
2022-06-03 08:09:47 -06:00
{
" name " : " status " ,
" type " : " options " ,
" options " : [
2023-03-22 00:57:20 +08:00
{ " display_name " : " firing " , " value " : AlertGroup . NEW } ,
2022-06-03 08:09:47 -06:00
{ " display_name " : " acknowledged " , " value " : AlertGroup . ACKNOWLEDGED } ,
{ " display_name " : " resolved " , " value " : AlertGroup . RESOLVED } ,
{ " display_name " : " silenced " , " value " : AlertGroup . SILENCED } ,
] ,
} ,
# {'name': 'is_root', 'type': 'boolean', 'default': True},
{
" name " : " started_at " ,
" type " : " daterange " ,
" default " : default_datetime_range ,
} ,
{
" name " : " resolved_at " ,
" type " : " daterange " ,
" default " : default_datetime_range ,
} ,
{
" name " : " with_resolution_note " ,
" type " : " boolean " ,
" default " : " true " ,
} ,
2023-01-30 09:08:18 -03:00
{
" name " : " mine " ,
" type " : " boolean " ,
" default " : " true " ,
2023-05-02 13:50:03 +08:00
" description " : f " This filter works only for last { AlertGroupFilter . FILTER_BY_INVOLVED_USERS_ALERT_GROUPS_CUTOFF } alert groups you ' re involved in. " ,
2023-01-30 09:08:18 -03:00
} ,
2022-06-03 08:09:47 -06:00
]
if filter_name is not None :
filter_options = list ( filter ( lambda f : filter_name in f [ " name " ] , filter_options ) )
return Response ( filter_options )
@action ( methods = [ " post " ] , detail = False )
def bulk_action ( self , request ) :
alert_group_public_pks = self . request . data . get ( " alert_group_pks " , [ ] )
action_with_incidents = self . request . data . get ( " action " , None )
delay = self . request . data . get ( " delay " )
kwargs = { }
if action_with_incidents not in AlertGroup . BULK_ACTIONS :
return Response ( " Unknown action " , status = status . HTTP_400_BAD_REQUEST )
if action_with_incidents == AlertGroup . SILENCE :
if delay is None :
raise BadRequest ( detail = " Please specify a delay for silence " )
kwargs [ " silence_delay " ] = delay
2023-07-18 13:48:34 +02:00
alert_groups = AlertGroup . objects . filter (
2022-07-27 12:14:59 +01:00
channel__organization = self . request . auth . organization , public_primary_key__in = alert_group_public_pks
)
2022-06-03 08:09:47 -06:00
kwargs [ " user " ] = self . request . user
kwargs [ " alert_groups " ] = alert_groups
method = getattr ( AlertGroup , f " bulk_ { action_with_incidents } " )
method ( * * kwargs )
return Response ( status = status . HTTP_200_OK )
@action ( methods = [ " get " ] , detail = False )
def bulk_action_options ( self , request ) :
return Response (
[ { " value " : action_name , " display_name " : action_name } for action_name in AlertGroup . BULK_ACTIONS ]
)
# This method is required for PreviewTemplateMixin
2023-04-18 11:57:40 +08:00
def get_alert_to_template ( self , payload = None ) :
2022-06-03 08:09:47 -06:00
return self . get_object ( ) . alerts . first ( )