From e0816dd1ff9a656ea992f8be3301b96ce9c06afb Mon Sep 17 00:00:00 2001 From: Matias Bordese Date: Fri, 11 Oct 2024 16:39:39 -0300 Subject: [PATCH] Minor optimizations for public API endpoints (#5172) - Add/update eager loading for public API serializers - Use cached final schedule to get currently on-call users --- engine/apps/public_api/serializers/escalation_chains.py | 5 ++++- .../apps/public_api/serializers/escalation_policies.py | 8 +++++++- engine/apps/public_api/serializers/integrations.py | 2 +- engine/apps/public_api/serializers/on_call_shifts.py | 2 +- engine/apps/public_api/serializers/routes.py | 5 ++++- engine/apps/public_api/serializers/schedules_base.py | 9 +++++++-- engine/apps/public_api/serializers/schedules_web.py | 3 +-- engine/apps/public_api/serializers/webhooks.py | 5 ++++- engine/apps/public_api/views/escalation_chains.py | 1 + engine/apps/public_api/views/integrations.py | 2 -- engine/apps/public_api/views/routes.py | 1 + engine/apps/public_api/views/schedules.py | 1 + engine/apps/public_api/views/webhooks.py | 1 + engine/apps/schedules/ical_utils.py | 8 +++++++- 14 files changed, 40 insertions(+), 13 deletions(-) diff --git a/engine/apps/public_api/serializers/escalation_chains.py b/engine/apps/public_api/serializers/escalation_chains.py index 6563036f..298cde09 100644 --- a/engine/apps/public_api/serializers/escalation_chains.py +++ b/engine/apps/public_api/serializers/escalation_chains.py @@ -2,14 +2,17 @@ from rest_framework import serializers from apps.alerts.models import EscalationChain from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField +from common.api_helpers.mixins import EagerLoadingMixin from common.api_helpers.utils import CurrentOrganizationDefault -class EscalationChainSerializer(serializers.ModelSerializer): +class EscalationChainSerializer(EagerLoadingMixin, serializers.ModelSerializer): id = serializers.ReadOnlyField(source="public_primary_key") organization = serializers.HiddenField(default=CurrentOrganizationDefault()) team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team") + SELECT_RELATED = ["organization", "team"] + class Meta: model = EscalationChain fields = ( diff --git a/engine/apps/public_api/serializers/escalation_policies.py b/engine/apps/public_api/serializers/escalation_policies.py index 54fb35ad..ad0abe78 100644 --- a/engine/apps/public_api/serializers/escalation_policies.py +++ b/engine/apps/public_api/serializers/escalation_policies.py @@ -107,7 +107,13 @@ class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer): ] PREFETCH_RELATED = ["notify_to_users_queue"] - SELECT_RELATED = ["escalation_chain"] + SELECT_RELATED = [ + "custom_webhook", + "escalation_chain", + "notify_schedule", + "notify_to_group", + "notify_to_team_members", + ] @cached_property def escalation_chain(self): diff --git a/engine/apps/public_api/serializers/integrations.py b/engine/apps/public_api/serializers/integrations.py index 7655a692..b16aeb54 100644 --- a/engine/apps/public_api/serializers/integrations.py +++ b/engine/apps/public_api/serializers/integrations.py @@ -85,7 +85,7 @@ class IntegrationSerializer(EagerLoadingMixin, serializers.ModelSerializer, Main description_short = serializers.CharField(max_length=250, required=False, allow_null=True) PREFETCH_RELATED = ["channel_filters"] - SELECT_RELATED = ["organization", "integration_heartbeat"] + SELECT_RELATED = ["organization", "integration_heartbeat", "team"] class Meta: model = AlertReceiveChannel diff --git a/engine/apps/public_api/serializers/on_call_shifts.py b/engine/apps/public_api/serializers/on_call_shifts.py index 92f073b8..ed86f623 100644 --- a/engine/apps/public_api/serializers/on_call_shifts.py +++ b/engine/apps/public_api/serializers/on_call_shifts.py @@ -122,7 +122,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer "source": {"required": False, "write_only": True}, } - SELECT_RELATED = ["schedule"] + SELECT_RELATED = ["organization", "team", "schedule"] PREFETCH_RELATED = ["schedules", "users"] def create(self, validated_data): diff --git a/engine/apps/public_api/serializers/routes.py b/engine/apps/public_api/serializers/routes.py index 0b6468db..e409a9d5 100644 --- a/engine/apps/public_api/serializers/routes.py +++ b/engine/apps/public_api/serializers/routes.py @@ -4,6 +4,7 @@ from apps.alerts.models import AlertReceiveChannel, ChannelFilter, EscalationCha from apps.base.messaging import get_messaging_backend_from_id, get_messaging_backends from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField from common.api_helpers.exceptions import BadRequest +from common.api_helpers.mixins import EagerLoadingMixin from common.api_helpers.utils import valid_jinja_template_for_serializer_method_field from common.jinja_templater.apply_jinja_template import JinjaTemplateError from common.ordered_model.serializer import OrderedModelSerializer @@ -129,7 +130,7 @@ class RoutingTypeField(fields.CharField): raise BadRequest(detail="Invalid route type") -class ChannelFilterSerializer(BaseChannelFilterSerializer): +class ChannelFilterSerializer(EagerLoadingMixin, BaseChannelFilterSerializer): id = serializers.CharField(read_only=True, source="public_primary_key") slack = serializers.DictField(required=False) telegram = serializers.DictField(required=False) @@ -146,6 +147,8 @@ class ChannelFilterSerializer(BaseChannelFilterSerializer): is_the_last_route = serializers.BooleanField(read_only=True, source="is_default") + SELECT_RELATED = ["alert_receive_channel", "escalation_chain"] + class Meta: model = ChannelFilter fields = OrderedModelSerializer.Meta.fields + [ diff --git a/engine/apps/public_api/serializers/schedules_base.py b/engine/apps/public_api/serializers/schedules_base.py index 5ea13cb4..2dabd254 100644 --- a/engine/apps/public_api/serializers/schedules_base.py +++ b/engine/apps/public_api/serializers/schedules_base.py @@ -6,21 +6,26 @@ from apps.schedules.ical_utils import list_users_to_notify_from_ical from apps.slack.models import SlackUserGroup from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField from common.api_helpers.exceptions import BadRequest +from common.api_helpers.mixins import EagerLoadingMixin -class ScheduleBaseSerializer(serializers.ModelSerializer): +class ScheduleBaseSerializer(EagerLoadingMixin, serializers.ModelSerializer): id = serializers.CharField(read_only=True, source="public_primary_key") on_call_now = serializers.SerializerMethodField() slack = serializers.DictField(required=False) team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team") + SELECT_RELATED = ["team"] + def create(self, validated_data): validated_data = self._correct_validated_data(validated_data) validated_data["organization"] = self.context["request"].auth.organization return super().create(validated_data) def get_on_call_now(self, obj): - users_on_call = list_users_to_notify_from_ical(obj, datetime.datetime.now(datetime.timezone.utc)) + users_on_call = list_users_to_notify_from_ical( + obj, datetime.datetime.now(datetime.timezone.utc), from_cached_final=True + ) if users_on_call is not None: return [user.public_primary_key for user in users_on_call] else: diff --git a/engine/apps/public_api/serializers/schedules_web.py b/engine/apps/public_api/serializers/schedules_web.py index 8967326b..ed390db3 100644 --- a/engine/apps/public_api/serializers/schedules_web.py +++ b/engine/apps/public_api/serializers/schedules_web.py @@ -5,11 +5,10 @@ from apps.schedules.tasks import ( schedule_notify_about_empty_shifts_in_schedule, schedule_notify_about_gaps_in_schedule, ) -from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField, TimeZoneField, UsersFilteredByOrganizationField +from common.api_helpers.custom_fields import TimeZoneField, UsersFilteredByOrganizationField class ScheduleWebSerializer(ScheduleBaseSerializer): - team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team") time_zone = TimeZoneField(required=True) shifts = UsersFilteredByOrganizationField( queryset=CustomOnCallShift.objects, diff --git a/engine/apps/public_api/serializers/webhooks.py b/engine/apps/public_api/serializers/webhooks.py index f11c8789..40513c5c 100644 --- a/engine/apps/public_api/serializers/webhooks.py +++ b/engine/apps/public_api/serializers/webhooks.py @@ -8,6 +8,7 @@ from apps.webhooks.models.webhook import PUBLIC_WEBHOOK_HTTP_METHODS, WEBHOOK_FI from apps.webhooks.presets.preset_options import WebhookPresetOptions from common.api_helpers.custom_fields import IntegrationFilteredByOrganizationField, TeamPrimaryKeyRelatedField from common.api_helpers.exceptions import BadRequest +from common.api_helpers.mixins import EagerLoadingMixin from common.api_helpers.utils import CurrentOrganizationDefault, CurrentTeamDefault, CurrentUserDefault from common.jinja_templater import apply_jinja_template from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning @@ -48,7 +49,7 @@ class WebhookResponseSerializer(serializers.ModelSerializer): ] -class WebhookCreateSerializer(serializers.ModelSerializer): +class WebhookCreateSerializer(EagerLoadingMixin, serializers.ModelSerializer): id = serializers.CharField(read_only=True, source="public_primary_key") organization = serializers.HiddenField(default=CurrentOrganizationDefault()) team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault()) @@ -58,6 +59,8 @@ class WebhookCreateSerializer(serializers.ModelSerializer): source="filtered_integrations", many=True, required=False ) + SELECT_RELATED = ["organization", "team"] + class Meta: model = Webhook fields = [ diff --git a/engine/apps/public_api/views/escalation_chains.py b/engine/apps/public_api/views/escalation_chains.py index 88cf24ba..d8f93513 100644 --- a/engine/apps/public_api/views/escalation_chains.py +++ b/engine/apps/public_api/views/escalation_chains.py @@ -34,6 +34,7 @@ class EscalationChainView(RateLimitHeadersMixin, ModelViewSet): if name is not None: queryset = queryset.filter(name=name) + queryset = self.serializer_class.setup_eager_loading(queryset) return queryset.order_by("id") def get_object(self): diff --git a/engine/apps/public_api/views/integrations.py b/engine/apps/public_api/views/integrations.py index 358f5420..ed17f9aa 100644 --- a/engine/apps/public_api/views/integrations.py +++ b/engine/apps/public_api/views/integrations.py @@ -1,4 +1,3 @@ -from django.db.models import Count from django_filters import rest_framework as filters from rest_framework.exceptions import NotFound from rest_framework.permissions import IsAuthenticated @@ -47,7 +46,6 @@ class IntegrationView( queryset = queryset.filter(verbal_name=name) queryset = self.filter_queryset(queryset) queryset = self.serializer_class.setup_eager_loading(queryset) - queryset = queryset.annotate(alert_groups_count_annotated=Count("alert_groups", distinct=True)) return queryset diff --git a/engine/apps/public_api/views/routes.py b/engine/apps/public_api/views/routes.py index 52d62eda..895e016e 100644 --- a/engine/apps/public_api/views/routes.py +++ b/engine/apps/public_api/views/routes.py @@ -37,6 +37,7 @@ class ChannelFilterView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewS queryset = ChannelFilter.objects.filter( alert_receive_channel__organization=self.request.auth.organization, alert_receive_channel__deleted_at=None ) + queryset = self.serializer_class.setup_eager_loading(queryset) if integration_id: queryset = queryset.filter(alert_receive_channel__public_primary_key=integration_id) diff --git a/engine/apps/public_api/views/schedules.py b/engine/apps/public_api/views/schedules.py index 53b414dc..e1b83cf5 100644 --- a/engine/apps/public_api/views/schedules.py +++ b/engine/apps/public_api/views/schedules.py @@ -57,6 +57,7 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo if name is not None: queryset = queryset.filter(name=name) + queryset = self.serializer_class.setup_eager_loading(queryset) return queryset.order_by("id") def get_object(self): diff --git a/engine/apps/public_api/views/webhooks.py b/engine/apps/public_api/views/webhooks.py index e8d7ed58..4773e2c3 100644 --- a/engine/apps/public_api/views/webhooks.py +++ b/engine/apps/public_api/views/webhooks.py @@ -42,6 +42,7 @@ class WebhooksView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet): if webhook_name: queryset = queryset.filter(name=webhook_name) + queryset = self.serializer_class.setup_eager_loading(queryset) return queryset.order_by("id") def get_object(self): diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index c3917324..00e779fc 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -344,6 +344,7 @@ def list_of_empty_shifts_in_schedule( def list_users_to_notify_from_ical( schedule: "OnCallSchedule", events_datetime: typing.Optional[datetime.datetime] = None, + from_cached_final: bool = False, ) -> typing.Sequence["User"]: """ Retrieve on-call users for the current time @@ -353,6 +354,7 @@ def list_users_to_notify_from_ical( schedule, events_datetime, events_datetime, + from_cached_final=from_cached_final, ) @@ -360,8 +362,12 @@ def list_users_to_notify_from_ical_for_period( schedule: "OnCallSchedule", start_datetime: datetime.datetime, end_datetime: datetime.datetime, + from_cached_final: bool = False, ) -> typing.Sequence["User"]: - events = schedule.final_events(start_datetime, end_datetime) + if from_cached_final and schedule.cached_ical_final_schedule: + events = schedule.filter_events(start_datetime, end_datetime, from_cached_final=True) + else: + events = schedule.final_events(start_datetime, end_datetime) usernames: typing.List[str] = [] for event in events: usernames += [u["email"] for u in event.get("users", [])]