2024-01-25 13:46:55 -05:00
|
|
|
import enum
|
2023-12-05 14:58:05 -05:00
|
|
|
import logging
|
2024-05-03 10:00:06 -03:00
|
|
|
import time
|
2023-12-05 14:58:05 -05:00
|
|
|
import typing
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
from django.conf import settings
|
2023-12-13 10:00:18 +01:00
|
|
|
from django.core.exceptions import ObjectDoesNotExist
|
2022-12-20 12:41:34 +01:00
|
|
|
from fcm_django.api.rest_framework import FCMDeviceAuthorizedViewSet as BaseFCMDeviceAuthorizedViewSet
|
2023-10-30 14:44:18 +01:00
|
|
|
from rest_framework import mixins, status, viewsets
|
2023-12-05 14:58:05 -05:00
|
|
|
from rest_framework.exceptions import NotFound, ParseError
|
Mobile app settings backend (#1571)
# What this PR does
Adds mobile app settings support to OnCall backend.
- Adds a new Django model `MobileAppUserSettings` to store push
notification settings
- Adds a new endpoint `/mobile_app/v1/user_settings` to fetch/update
settings from the mobile app
Some additional info on implementation: at first I wanted to extend the
messaging backend system to allow storing / retrieving per-user data and
implement mobile app settings based on those changes. After some thought
I decided not to extend the messaging backend system and have this as
functionality specific to the `mobile_app` Django app. Currently the
messaging backend system is used by the backend and plugin UI, but
mobile app settings are specific only to the mobile app and not
configurable in the plugin UI.
**tldr: wanted to extend messaging backend system, but decided not to do
that**
# Usage
## Get settings via API
`GET /mobile_app/v1/user_settings`
Example response:
```json
{
"default_notification_sound_name": "default_sound", # sound name without file extension
"default_notification_volume_type": "constant",
"default_notification_volume": 0.8,
"default_notification_volume_override": false,
"important_notification_sound_name": "default_sound_important", # sound name without file extension
"important_notification_volume_type": "constant",
"important_notification_volume": 0.8,
"important_notification_override_dnd": true
}
```
## Update settings via API
`PUT /mobile_app/v1/user_settings` - see example response above for
payload shape.
Note that sound names must be passed without file extension. When
sending push notifications, the backend will add `.mp3` to sound names
and pass it to push notification data for Android. For iOS, sound names
will be suffixed with `.aiff` to be used by APNS.
## Get settings from notification data for Android
All the settings from example response will be available in push
notification data (along with `orgId`, `alertGroupId`, `title`, etc.).
Fields `default_notification_volume`,
`default_notification_volume_override` and
`important_notification_volume` , `important_notification_override_dnd`
will be converted to strings due to FCM limitations.
Fields `default_notification_sound_name` and
`important_notification_sound_name` will be suffixed with `.mp3` in push
notification data.
## iOS limitations
While Android push notifications are handled purely on the mobile app
side, iOS notifications are sent via APNS which imposes some
limitations.
- Notification volume cannot be overridden for non-critical
notifications (so fields `default_notification_volume_override` and
`default_notification_volume` will be disregarded for iOS notifications)
- It's not possible to control volume type (i.e. "constant" vs
"intensifying") via APNS. A possible workaround is to have different
sound files for "constant" and "intensifying" and pass that as
`default_notification_sound_name` / `important_notification_sound_name`.
# Which issue(s) this PR fixes
Related to https://github.com/grafana/oncall-private/issues/1602
# Checklist
- [x] Tests updated
- [x] No changelog updates since the changes are not user-facing
2023-03-22 14:47:18 +00:00
|
|
|
from rest_framework.permissions import IsAuthenticated
|
2024-01-17 16:29:47 -05:00
|
|
|
from rest_framework.request import Request
|
2022-11-28 12:50:58 +00:00
|
|
|
from rest_framework.response import Response
|
|
|
|
|
from rest_framework.views import APIView
|
2022-11-23 15:56:43 +00:00
|
|
|
|
2022-11-28 12:50:58 +00:00
|
|
|
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication, MobileAppVerificationTokenAuthentication
|
2023-12-13 10:00:18 +01:00
|
|
|
from apps.mobile_app.models import FCMDevice, MobileAppAuthToken, MobileAppUserSettings
|
|
|
|
|
from apps.mobile_app.serializers import FCMDeviceSerializer, MobileAppUserSettingsSerializer
|
2024-01-25 13:46:55 -05:00
|
|
|
from common.cloud_auth_api.client import CloudAuthApiClient, CloudAuthApiException
|
2022-11-23 15:56:43 +00:00
|
|
|
|
2023-12-05 14:58:05 -05:00
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
|
from apps.user_management.models import Organization, User
|
|
|
|
|
|
|
|
|
|
|
2024-05-03 10:00:06 -03:00
|
|
|
PROXY_REQUESTS_TIMEOUT = 5
|
|
|
|
|
|
|
|
|
|
|
2023-12-05 14:58:05 -05:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
2022-11-23 15:56:43 +00:00
|
|
|
|
2022-12-20 12:41:34 +01:00
|
|
|
class FCMDeviceAuthorizedViewSet(BaseFCMDeviceAuthorizedViewSet):
|
2022-11-23 15:56:43 +00:00
|
|
|
authentication_classes = (MobileAppAuthTokenAuthentication,)
|
2023-12-13 10:00:18 +01:00
|
|
|
serializer_class = FCMDeviceSerializer
|
|
|
|
|
model = FCMDevice
|
|
|
|
|
|
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
|
|
|
"""Overrides `create` from BaseFCMDeviceAuthorizedViewSet to add filtering by user on getting instance"""
|
|
|
|
|
serializer = None
|
|
|
|
|
is_update = False
|
|
|
|
|
if settings.FCM_DJANGO_SETTINGS["UPDATE_ON_DUPLICATE_REG_ID"] and "registration_id" in request.data:
|
|
|
|
|
instance = self.model.objects.filter(
|
|
|
|
|
registration_id=request.data["registration_id"], user=self.request.user
|
|
|
|
|
).first()
|
|
|
|
|
if instance:
|
|
|
|
|
serializer = self.get_serializer(instance, data=request.data)
|
|
|
|
|
is_update = True
|
|
|
|
|
if not serializer:
|
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
|
|
|
|
|
|
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
|
if is_update:
|
|
|
|
|
self.perform_update(serializer)
|
|
|
|
|
return Response(serializer.data)
|
|
|
|
|
else:
|
|
|
|
|
self.perform_create(serializer)
|
|
|
|
|
headers = self.get_success_headers(serializer.data)
|
|
|
|
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
|
|
|
|
def get_object(self):
|
|
|
|
|
"""Overrides original method to add filtering by user"""
|
|
|
|
|
try:
|
|
|
|
|
obj = self.model.objects.get(registration_id=self.kwargs["registration_id"], user=self.request.user)
|
|
|
|
|
except ObjectDoesNotExist:
|
|
|
|
|
raise NotFound
|
|
|
|
|
# May raise a permission denied
|
|
|
|
|
self.check_object_permissions(self.request, obj)
|
|
|
|
|
return obj
|
2022-11-28 12:50:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MobileAppAuthTokenAPIView(APIView):
|
|
|
|
|
authentication_classes = (MobileAppVerificationTokenAuthentication,)
|
|
|
|
|
|
|
|
|
|
def get(self, request):
|
|
|
|
|
try:
|
|
|
|
|
token = MobileAppAuthToken.objects.get(user=self.request.user)
|
|
|
|
|
except MobileAppAuthToken.DoesNotExist:
|
|
|
|
|
raise NotFound
|
|
|
|
|
|
|
|
|
|
response = {
|
|
|
|
|
"token_id": token.id,
|
|
|
|
|
"user_id": token.user_id,
|
|
|
|
|
"organization_id": token.organization_id,
|
|
|
|
|
"created_at": token.created_at,
|
|
|
|
|
"revoked_at": token.revoked_at,
|
2023-12-13 10:00:18 +01:00
|
|
|
"stack_slug": self.request.auth.organization.stack_slug,
|
2022-11-28 12:50:58 +00:00
|
|
|
}
|
|
|
|
|
return Response(response, status=status.HTTP_200_OK)
|
|
|
|
|
|
|
|
|
|
def post(self, request):
|
|
|
|
|
# If token already exists revoke it
|
|
|
|
|
try:
|
|
|
|
|
token = MobileAppAuthToken.objects.get(user=self.request.user)
|
|
|
|
|
token.delete()
|
|
|
|
|
except MobileAppAuthToken.DoesNotExist:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
instance, token = MobileAppAuthToken.create_auth_token(self.request.user, self.request.user.organization)
|
2023-12-13 10:00:18 +01:00
|
|
|
data = {
|
|
|
|
|
"id": instance.pk,
|
|
|
|
|
"token": token,
|
|
|
|
|
"created_at": instance.created_at,
|
|
|
|
|
"stack_slug": self.request.auth.organization.stack_slug,
|
|
|
|
|
}
|
2022-11-28 12:50:58 +00:00
|
|
|
return Response(data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
|
|
|
|
def delete(self, request):
|
|
|
|
|
try:
|
|
|
|
|
token = MobileAppAuthToken.objects.get(user=self.request.user)
|
|
|
|
|
token.delete()
|
|
|
|
|
except MobileAppAuthToken.DoesNotExist:
|
|
|
|
|
raise NotFound
|
|
|
|
|
|
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
Mobile app settings backend (#1571)
# What this PR does
Adds mobile app settings support to OnCall backend.
- Adds a new Django model `MobileAppUserSettings` to store push
notification settings
- Adds a new endpoint `/mobile_app/v1/user_settings` to fetch/update
settings from the mobile app
Some additional info on implementation: at first I wanted to extend the
messaging backend system to allow storing / retrieving per-user data and
implement mobile app settings based on those changes. After some thought
I decided not to extend the messaging backend system and have this as
functionality specific to the `mobile_app` Django app. Currently the
messaging backend system is used by the backend and plugin UI, but
mobile app settings are specific only to the mobile app and not
configurable in the plugin UI.
**tldr: wanted to extend messaging backend system, but decided not to do
that**
# Usage
## Get settings via API
`GET /mobile_app/v1/user_settings`
Example response:
```json
{
"default_notification_sound_name": "default_sound", # sound name without file extension
"default_notification_volume_type": "constant",
"default_notification_volume": 0.8,
"default_notification_volume_override": false,
"important_notification_sound_name": "default_sound_important", # sound name without file extension
"important_notification_volume_type": "constant",
"important_notification_volume": 0.8,
"important_notification_override_dnd": true
}
```
## Update settings via API
`PUT /mobile_app/v1/user_settings` - see example response above for
payload shape.
Note that sound names must be passed without file extension. When
sending push notifications, the backend will add `.mp3` to sound names
and pass it to push notification data for Android. For iOS, sound names
will be suffixed with `.aiff` to be used by APNS.
## Get settings from notification data for Android
All the settings from example response will be available in push
notification data (along with `orgId`, `alertGroupId`, `title`, etc.).
Fields `default_notification_volume`,
`default_notification_volume_override` and
`important_notification_volume` , `important_notification_override_dnd`
will be converted to strings due to FCM limitations.
Fields `default_notification_sound_name` and
`important_notification_sound_name` will be suffixed with `.mp3` in push
notification data.
## iOS limitations
While Android push notifications are handled purely on the mobile app
side, iOS notifications are sent via APNS which imposes some
limitations.
- Notification volume cannot be overridden for non-critical
notifications (so fields `default_notification_volume_override` and
`default_notification_volume` will be disregarded for iOS notifications)
- It's not possible to control volume type (i.e. "constant" vs
"intensifying") via APNS. A possible workaround is to have different
sound files for "constant" and "intensifying" and pass that as
`default_notification_sound_name` / `important_notification_sound_name`.
# Which issue(s) this PR fixes
Related to https://github.com/grafana/oncall-private/issues/1602
# Checklist
- [x] Tests updated
- [x] No changelog updates since the changes are not user-facing
2023-03-22 14:47:18 +00:00
|
|
|
|
|
|
|
|
|
2023-10-30 14:44:18 +01:00
|
|
|
class MobileAppUserSettingsViewSet(
|
|
|
|
|
mixins.RetrieveModelMixin,
|
|
|
|
|
mixins.UpdateModelMixin,
|
|
|
|
|
viewsets.GenericViewSet,
|
|
|
|
|
):
|
Mobile app settings backend (#1571)
# What this PR does
Adds mobile app settings support to OnCall backend.
- Adds a new Django model `MobileAppUserSettings` to store push
notification settings
- Adds a new endpoint `/mobile_app/v1/user_settings` to fetch/update
settings from the mobile app
Some additional info on implementation: at first I wanted to extend the
messaging backend system to allow storing / retrieving per-user data and
implement mobile app settings based on those changes. After some thought
I decided not to extend the messaging backend system and have this as
functionality specific to the `mobile_app` Django app. Currently the
messaging backend system is used by the backend and plugin UI, but
mobile app settings are specific only to the mobile app and not
configurable in the plugin UI.
**tldr: wanted to extend messaging backend system, but decided not to do
that**
# Usage
## Get settings via API
`GET /mobile_app/v1/user_settings`
Example response:
```json
{
"default_notification_sound_name": "default_sound", # sound name without file extension
"default_notification_volume_type": "constant",
"default_notification_volume": 0.8,
"default_notification_volume_override": false,
"important_notification_sound_name": "default_sound_important", # sound name without file extension
"important_notification_volume_type": "constant",
"important_notification_volume": 0.8,
"important_notification_override_dnd": true
}
```
## Update settings via API
`PUT /mobile_app/v1/user_settings` - see example response above for
payload shape.
Note that sound names must be passed without file extension. When
sending push notifications, the backend will add `.mp3` to sound names
and pass it to push notification data for Android. For iOS, sound names
will be suffixed with `.aiff` to be used by APNS.
## Get settings from notification data for Android
All the settings from example response will be available in push
notification data (along with `orgId`, `alertGroupId`, `title`, etc.).
Fields `default_notification_volume`,
`default_notification_volume_override` and
`important_notification_volume` , `important_notification_override_dnd`
will be converted to strings due to FCM limitations.
Fields `default_notification_sound_name` and
`important_notification_sound_name` will be suffixed with `.mp3` in push
notification data.
## iOS limitations
While Android push notifications are handled purely on the mobile app
side, iOS notifications are sent via APNS which imposes some
limitations.
- Notification volume cannot be overridden for non-critical
notifications (so fields `default_notification_volume_override` and
`default_notification_volume` will be disregarded for iOS notifications)
- It's not possible to control volume type (i.e. "constant" vs
"intensifying") via APNS. A possible workaround is to have different
sound files for "constant" and "intensifying" and pass that as
`default_notification_sound_name` / `important_notification_sound_name`.
# Which issue(s) this PR fixes
Related to https://github.com/grafana/oncall-private/issues/1602
# Checklist
- [x] Tests updated
- [x] No changelog updates since the changes are not user-facing
2023-03-22 14:47:18 +00:00
|
|
|
authentication_classes = (MobileAppAuthTokenAuthentication,)
|
|
|
|
|
permission_classes = (IsAuthenticated,)
|
|
|
|
|
serializer_class = MobileAppUserSettingsSerializer
|
|
|
|
|
|
|
|
|
|
def get_object(self):
|
|
|
|
|
mobile_app_settings, _ = MobileAppUserSettings.objects.get_or_create(user=self.request.user)
|
|
|
|
|
return mobile_app_settings
|
2023-10-30 14:44:18 +01:00
|
|
|
|
|
|
|
|
def notification_timing_options(self, request):
|
|
|
|
|
choices = [
|
|
|
|
|
{"value": item[0], "display_name": item[1]} for item in MobileAppUserSettings.NOTIFICATION_TIMING_CHOICES
|
|
|
|
|
]
|
|
|
|
|
return Response(choices)
|
2023-12-05 14:58:05 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MobileAppGatewayView(APIView):
|
|
|
|
|
authentication_classes = (MobileAppAuthTokenAuthentication,)
|
|
|
|
|
permission_classes = (IsAuthenticated,)
|
|
|
|
|
|
2024-01-25 13:46:55 -05:00
|
|
|
class SupportedDownstreamBackends(enum.StrEnum):
|
2023-12-05 14:58:05 -05:00
|
|
|
INCIDENT = "incident"
|
|
|
|
|
|
2024-01-25 13:46:55 -05:00
|
|
|
ALL_SUPPORTED_DOWNSTREAM_BACKENDS = list(SupportedDownstreamBackends)
|
2023-12-05 14:58:05 -05:00
|
|
|
|
2024-01-17 16:29:47 -05:00
|
|
|
def initial(self, request: Request, *args, **kwargs):
|
2023-12-06 10:57:07 +00:00
|
|
|
# If the mobile app gateway is not enabled, return a 404
|
|
|
|
|
if not settings.MOBILE_APP_GATEWAY_ENABLED:
|
|
|
|
|
raise NotFound
|
|
|
|
|
super().initial(request, *args, **kwargs)
|
|
|
|
|
|
2023-12-05 14:58:05 -05:00
|
|
|
@classmethod
|
2024-01-25 13:46:55 -05:00
|
|
|
def _get_auth_token(cls, downstream_backend: SupportedDownstreamBackends, user: "User") -> str:
|
2023-12-05 14:58:05 -05:00
|
|
|
"""
|
|
|
|
|
RS256 = asymmetric = public/private key pair
|
|
|
|
|
HS256 = symmetric = shared secret (don't use this)
|
|
|
|
|
"""
|
2024-01-25 15:14:08 -05:00
|
|
|
org = user.organization
|
2024-01-25 13:46:55 -05:00
|
|
|
token_scopes = {
|
|
|
|
|
cls.SupportedDownstreamBackends.INCIDENT: [CloudAuthApiClient.Scopes.INCIDENT_WRITE],
|
|
|
|
|
}[downstream_backend]
|
|
|
|
|
|
2024-04-11 10:45:21 -04:00
|
|
|
return f"{org.stack_id}:{CloudAuthApiClient().request_signed_token(user, token_scopes)}"
|
2023-12-05 14:58:05 -05:00
|
|
|
|
|
|
|
|
@classmethod
|
2024-01-25 13:46:55 -05:00
|
|
|
def _get_downstream_headers(
|
|
|
|
|
cls, request: Request, downstream_backend: SupportedDownstreamBackends, user: "User"
|
|
|
|
|
) -> typing.Dict[str, str]:
|
2024-01-17 16:29:47 -05:00
|
|
|
headers = {
|
2024-01-25 13:46:55 -05:00
|
|
|
"Authorization": f"Bearer {cls._get_auth_token(downstream_backend, user)}",
|
2023-12-05 14:58:05 -05:00
|
|
|
}
|
|
|
|
|
|
2024-01-17 16:29:47 -05:00
|
|
|
if (v := request.META.get("CONTENT_TYPE", None)) is not None:
|
|
|
|
|
headers["Content-Type"] = v
|
|
|
|
|
|
|
|
|
|
return headers
|
|
|
|
|
|
2023-12-05 14:58:05 -05:00
|
|
|
@classmethod
|
2024-01-25 13:46:55 -05:00
|
|
|
def _get_downstream_url(
|
|
|
|
|
cls, organization: "Organization", downstream_backend: SupportedDownstreamBackends, downstream_path: str
|
|
|
|
|
) -> str:
|
2023-12-05 14:58:05 -05:00
|
|
|
downstream_url = {
|
|
|
|
|
cls.SupportedDownstreamBackends.INCIDENT: organization.grafana_incident_backend_url,
|
|
|
|
|
}[downstream_backend]
|
|
|
|
|
|
|
|
|
|
if downstream_url is None:
|
|
|
|
|
raise ParseError(
|
|
|
|
|
f"Downstream URL not found for backend {downstream_backend} for organization {organization.pk}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return f"{downstream_url}/{downstream_path}"
|
|
|
|
|
|
2024-01-17 16:29:47 -05:00
|
|
|
def _proxy_request(self, request: Request, *args, **kwargs) -> Response:
|
2024-05-03 10:00:06 -03:00
|
|
|
request_start = time.perf_counter()
|
2023-12-05 14:58:05 -05:00
|
|
|
downstream_backend = kwargs["downstream_backend"]
|
|
|
|
|
downstream_path = kwargs["downstream_path"]
|
|
|
|
|
method = request.method
|
|
|
|
|
user = request.user
|
2024-08-28 14:56:59 -03:00
|
|
|
org = user.organization
|
2023-12-05 14:58:05 -05:00
|
|
|
|
|
|
|
|
if downstream_backend not in self.ALL_SUPPORTED_DOWNSTREAM_BACKENDS:
|
|
|
|
|
raise NotFound(f"Downstream backend {downstream_backend} not supported")
|
|
|
|
|
|
2024-08-28 14:56:59 -03:00
|
|
|
if downstream_backend == self.SupportedDownstreamBackends.INCIDENT and not org.is_grafana_incident_enabled:
|
|
|
|
|
raise NotFound(f"Incident is not enabled for organization {org.pk}")
|
|
|
|
|
|
2023-12-05 14:58:05 -05:00
|
|
|
downstream_url = self._get_downstream_url(user.organization, downstream_backend, downstream_path)
|
2024-01-26 10:48:35 -05:00
|
|
|
|
|
|
|
|
log_msg_common = f"{downstream_backend} request to {method} {downstream_url}"
|
|
|
|
|
logger.info(f"Proxying {log_msg_common}")
|
|
|
|
|
|
2023-12-05 14:58:05 -05:00
|
|
|
downstream_request_handler = getattr(requests, method.lower())
|
|
|
|
|
|
2024-08-28 14:56:59 -03:00
|
|
|
final_status = None
|
|
|
|
|
response_data = None
|
2023-12-05 14:58:05 -05:00
|
|
|
try:
|
|
|
|
|
downstream_response = downstream_request_handler(
|
|
|
|
|
downstream_url,
|
2024-01-17 16:29:47 -05:00
|
|
|
data=request.body,
|
2023-12-05 14:58:05 -05:00
|
|
|
params=request.query_params.dict(),
|
2024-01-25 13:46:55 -05:00
|
|
|
headers=self._get_downstream_headers(request, downstream_backend, user),
|
2024-05-03 10:00:06 -03:00
|
|
|
timeout=PROXY_REQUESTS_TIMEOUT, # set a timeout to prevent hanging
|
2023-12-05 14:58:05 -05:00
|
|
|
)
|
2024-05-03 10:00:06 -03:00
|
|
|
final_status = downstream_response.status_code
|
2024-08-28 14:56:59 -03:00
|
|
|
response_data = downstream_response.json()
|
2024-01-26 10:48:35 -05:00
|
|
|
logger.info(f"Successfully proxied {log_msg_common}")
|
2024-08-28 14:56:59 -03:00
|
|
|
# raise an exception if the response status code is not 2xx
|
|
|
|
|
# (exception handler will log and still return the response)
|
|
|
|
|
downstream_response.raise_for_status()
|
|
|
|
|
return Response(status=downstream_response.status_code, data=response_data)
|
2023-12-05 14:58:05 -05:00
|
|
|
except (
|
|
|
|
|
requests.exceptions.RequestException,
|
|
|
|
|
requests.exceptions.JSONDecodeError,
|
2024-05-03 10:00:06 -03:00
|
|
|
requests.exceptions.Timeout,
|
2024-01-25 13:46:55 -05:00
|
|
|
CloudAuthApiException,
|
2023-12-05 14:58:05 -05:00
|
|
|
) as e:
|
|
|
|
|
if isinstance(e, requests.exceptions.JSONDecodeError):
|
|
|
|
|
final_status = status.HTTP_400_BAD_REQUEST
|
2024-05-03 10:00:06 -03:00
|
|
|
elif isinstance(e, requests.exceptions.Timeout):
|
|
|
|
|
final_status = status.HTTP_504_GATEWAY_TIMEOUT
|
2024-08-28 14:56:59 -03:00
|
|
|
elif final_status is None:
|
2023-12-05 14:58:05 -05:00
|
|
|
final_status = status.HTTP_502_BAD_GATEWAY
|
|
|
|
|
|
|
|
|
|
logger.error(
|
|
|
|
|
(
|
|
|
|
|
f"MobileAppGatewayView: error while proxying request\n"
|
|
|
|
|
f"method={method}\n"
|
|
|
|
|
f"downstream_backend={downstream_backend}\n"
|
|
|
|
|
f"downstream_path={downstream_path}\n"
|
|
|
|
|
f"downstream_url={downstream_url}\n"
|
|
|
|
|
f"final_status={final_status}"
|
|
|
|
|
),
|
|
|
|
|
exc_info=True,
|
|
|
|
|
)
|
2024-08-28 14:56:59 -03:00
|
|
|
return Response(status=final_status, data=response_data)
|
2024-05-03 10:00:06 -03:00
|
|
|
finally:
|
|
|
|
|
request_end = time.perf_counter()
|
|
|
|
|
seconds = request_end - request_start
|
|
|
|
|
logging.info(
|
|
|
|
|
f"outbound latency={str(seconds)} status={final_status} "
|
|
|
|
|
f"method={method.upper()} url={downstream_url} "
|
|
|
|
|
f"slow={int(seconds > settings.SLOW_THRESHOLD_SECONDS)} "
|
|
|
|
|
)
|
2023-12-05 14:58:05 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
See the default `APIView.dispatch` for more info. Basically this just routes all requests for
|
|
|
|
|
ALL HTTP verbs to the `MobileAppGatewayView._proxy_request` method.
|
|
|
|
|
"""
|
|
|
|
|
for method in APIView.http_method_names:
|
|
|
|
|
setattr(MobileAppGatewayView, method.lower(), MobileAppGatewayView._proxy_request)
|