Plug in sms/phone cloud notifications
This commit is contained in:
parent
0cdd2d7b8b
commit
f68d3f2146
6 changed files with 106 additions and 29 deletions
|
|
@ -12,6 +12,7 @@ from apps.alerts.constants import NEXT_ESCALATION_DELAY
|
|||
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
|
||||
from apps.alerts.signals import user_notification_action_triggered_signal
|
||||
from apps.base.messaging import get_messaging_backend_from_id
|
||||
from apps.base.utils import live_settings
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
from .task_logger import task_logger
|
||||
|
|
@ -258,10 +259,20 @@ def perform_notification(log_record_pk):
|
|||
return
|
||||
|
||||
if notification_channel == UserNotificationPolicy.NotificationChannel.SMS:
|
||||
SMSMessage.send_sms(user, alert_group, notification_policy)
|
||||
SMSMessage.send_sms(
|
||||
user,
|
||||
alert_group,
|
||||
notification_policy,
|
||||
is_cloud_notification=live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED,
|
||||
)
|
||||
|
||||
elif notification_channel == UserNotificationPolicy.NotificationChannel.PHONE_CALL:
|
||||
PhoneCall.make_call(user, alert_group, notification_policy)
|
||||
PhoneCall.make_call(
|
||||
user,
|
||||
alert_group,
|
||||
notification_policy,
|
||||
is_cloud_notification=live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED,
|
||||
)
|
||||
|
||||
elif notification_channel == UserNotificationPolicy.NotificationChannel.TELEGRAM:
|
||||
if alert_group.notify_in_telegram_enabled is True:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class LiveSetting(models.Model):
|
|||
"SEND_ANONYMOUS_USAGE_STATS",
|
||||
"GRAFANA_CLOUD_ONCALL_TOKEN",
|
||||
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED",
|
||||
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED",
|
||||
)
|
||||
|
||||
DESCRIPTIONS = {
|
||||
|
|
@ -106,6 +107,7 @@ class LiveSetting(models.Model):
|
|||
),
|
||||
"GRAFANA_CLOUD_ONCALL_TOKEN": "Secret token for Grafana Cloud OnCall instance.",
|
||||
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED": "Enable hearbeat integration with Grafana Cloud OnCall.",
|
||||
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED": "Enable SMS/call notifications via Grafana Cloud OnCall",
|
||||
}
|
||||
|
||||
SECRET_SETTING_NAMES = (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# TODO: move to serializers
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
|
@ -11,7 +10,7 @@ from apps.twilioapp.models import PhoneCall, SMSMessage
|
|||
|
||||
class PhoneNotificationDataSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
message = serializers.CharField(max_length=200)
|
||||
message = serializers.CharField(max_length=1024)
|
||||
|
||||
|
||||
class MakeCallView(APIView):
|
||||
|
|
@ -21,27 +20,26 @@ class MakeCallView(APIView):
|
|||
# TODO: add ratelimit
|
||||
|
||||
def post(self, request):
|
||||
# TODO: Grafana Twilio:
|
||||
# 1. Validate user's email
|
||||
# 2. Validate payload: clean_markup, escape_for_twilio_phone_call
|
||||
# 3. Create LogRecord (User notification policy or implement new one)
|
||||
serializer = PhoneNotificationDataSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
response_data = {}
|
||||
organization = self.request.auth.organization
|
||||
# TODO: filter by verified phone number?
|
||||
user = organization.users.filter(email=serializer.validated_data["email"]).first()
|
||||
if not user or not user.verified_phone_number:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
user = organization.users.filter(
|
||||
email=serializer.validated_data["email"], _verified_phone_number__isnull=False
|
||||
).first()
|
||||
if user is None:
|
||||
response_data = {"error": "user-not-found"}
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data=response_data)
|
||||
|
||||
try:
|
||||
PhoneCall.make_grafana_cloud_call(user, serializer.validated_data["message"])
|
||||
except TwilioRestException:
|
||||
return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE, data=response_data)
|
||||
except PhoneCall.PhoneCallsLimitExceeded:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"error": "limit-exceeded"})
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
return Response(status=status.HTTP_200_OK, data=response_data)
|
||||
|
||||
|
||||
class SendSMSView(APIView):
|
||||
|
|
@ -52,17 +50,20 @@ class SendSMSView(APIView):
|
|||
serializer = PhoneNotificationDataSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
response_data = {}
|
||||
organization = self.request.auth.organization
|
||||
# TODO: filter by verified phone number?
|
||||
user = organization.users.filter(email=serializer.validated_data["email"]).first()
|
||||
if not user or not user.verified_phone_number:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
user = organization.users.filter(
|
||||
email=serializer.validated_data["email"], _verified_phone_number__isnull=False
|
||||
).first()
|
||||
if user is None:
|
||||
response_data = {"error": "user-not-found"}
|
||||
return Response(status=status.HTTP_404_NOT_FOUND, data=response_data)
|
||||
|
||||
try:
|
||||
SMSMessage.send_cloud_sms(user, serializer.validated_data["message"])
|
||||
SMSMessage.send_grafana_cloud_sms(user, serializer.validated_data["message"])
|
||||
except TwilioRestException:
|
||||
return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||
return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE, data=response_data)
|
||||
except SMSMessage.SMSLimitExceeded:
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={"error": "limit-exceeded"})
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
return Response(status=status.HTTP_200_OK, data=response_data)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from rest_framework import status
|
||||
from twilio.base.exceptions import TwilioRestException
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
|
|
@ -125,6 +129,9 @@ class PhoneCall(models.Model):
|
|||
class PhoneNumberNotVerifiedError(Exception):
|
||||
"""Phone number is not verified"""
|
||||
|
||||
class CloudSendError(Exception):
|
||||
"""Error making call through cloud"""
|
||||
|
||||
def process_digit(self, digit):
|
||||
"""The function process pressed digit at time of call to user
|
||||
|
||||
|
|
@ -148,14 +155,38 @@ class PhoneCall(models.Model):
|
|||
return bool(self.represents_alert_group.slack_message)
|
||||
|
||||
@classmethod
|
||||
def make_call(cls, user, alert_group, notification_policy):
|
||||
def _make_cloud_call(cls, user, message_body):
|
||||
url = urljoin(settings.GRAFANA_CLOUD_ONCALL_API_URL, "api/v1/make_call")
|
||||
auth = {"Authorization": settings.GRAFANA_CLOUD_ONCALL_TOKEN}
|
||||
data = {
|
||||
"email": user.email,
|
||||
"message": message_body,
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, headers=auth, data=data, timeout=5)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Unable to make call through cloud. Request exception {str(e)}")
|
||||
raise PhoneCall.CloudSendError("Unable to make call through cloud: request failed")
|
||||
|
||||
if response.status_code == status.HTTP_400_BAD_REQUEST and response.json().get("error") == "limit-exceeded":
|
||||
raise PhoneCall.PhoneCallsLimitExceeded("Organization calls limit exceeded")
|
||||
elif response.status_code == status.HTTP_404_NOT_FOUND:
|
||||
raise PhoneCall.CloudSendError("Unable to make call through cloud: user not found")
|
||||
else:
|
||||
raise PhoneCall.CloudSendError("Unable to make call through cloud: server error")
|
||||
|
||||
@classmethod
|
||||
def make_call(cls, user, alert_group, notification_policy, is_cloud_notification=False):
|
||||
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
||||
log_record = None
|
||||
renderer = AlertGroupPhoneCallRenderer(alert_group)
|
||||
message_body = renderer.render()
|
||||
try:
|
||||
cls._make_call(user, message_body, alert_group=alert_group, notification_policy=notification_policy)
|
||||
except TwilioRestException:
|
||||
if is_cloud_notification:
|
||||
cls._make_cloud_call(user, message_body)
|
||||
else:
|
||||
cls._make_call(user, message_body, alert_group=alert_group, notification_policy=notification_policy)
|
||||
except (TwilioRestException, PhoneCall.CloudSendError):
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from rest_framework import status
|
||||
from twilio.base.exceptions import TwilioRestException
|
||||
|
||||
from apps.alerts.incident_appearance.renderers.sms_renderer import AlertGroupSmsRenderer
|
||||
|
|
@ -108,20 +112,47 @@ class SMSMessage(models.Model):
|
|||
class PhoneNumberNotVerifiedError(Exception):
|
||||
"""Phone number is not verified"""
|
||||
|
||||
class CloudSendError(Exception):
|
||||
"""SMS sending through cloud error"""
|
||||
|
||||
@property
|
||||
def created_for_slack(self):
|
||||
return bool(self.represents_alert_group.slack_message)
|
||||
|
||||
@classmethod
|
||||
def send_sms(cls, user, alert_group, notification_policy):
|
||||
def _send_cloud_sms(cls, user, message_body):
|
||||
url = urljoin(settings.GRAFANA_CLOUD_ONCALL_API_URL, "api/v1/send_sms")
|
||||
auth = {"Authorization": settings.GRAFANA_CLOUD_ONCALL_TOKEN}
|
||||
data = {
|
||||
"email": user.email,
|
||||
"message": message_body,
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, headers=auth, data=data, timeout=5)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Unable to send SMS through cloud. Request exception {str(e)}")
|
||||
raise SMSMessage.CloudSendError("Unable to send SMS through cloud: request failed")
|
||||
|
||||
if response.status_code == status.HTTP_400_BAD_REQUEST and response.json().get("error") == "limit-exceeded":
|
||||
raise SMSMessage.SMSLimitExceeded("Organization sms limit exceeded")
|
||||
elif response.status_code == status.HTTP_404_NOT_FOUND:
|
||||
raise SMSMessage.CloudSendError("Unable to send SMS through cloud: user not found")
|
||||
else:
|
||||
raise SMSMessage.CloudSendError("Unable to send SMS through cloud: server error")
|
||||
|
||||
@classmethod
|
||||
def send_sms(cls, user, alert_group, notification_policy, is_cloud_notification=False):
|
||||
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
||||
|
||||
log_record = None
|
||||
renderer = AlertGroupSmsRenderer(alert_group)
|
||||
message_body = renderer.render()
|
||||
try:
|
||||
cls._send_sms(user, message_body, alert_group=alert_group, notification_policy=notification_policy)
|
||||
except TwilioRestException:
|
||||
if is_cloud_notification:
|
||||
cls._send_cloud_sms(user, message_body)
|
||||
else:
|
||||
cls._send_sms(user, message_body, alert_group=alert_group, notification_policy=notification_policy)
|
||||
except (TwilioRestException, SMSMessage.CloudSendError):
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
|
|
|
|||
|
|
@ -412,6 +412,7 @@ SELF_HOSTED_SETTINGS = {
|
|||
GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get("GRAFANA_CLOUD_ONCALL_API_URL", "https://a-prod-us-central-0.grafana.net")
|
||||
GRAFANA_CLOUD_ONCALL_TOKEN = os.environ.get("GRAFANA_CLOUD_ONCALL_TOKEN", None)
|
||||
GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True)
|
||||
GRAFANA_CLOUD_NOTIFICATIONS_ENABLED = getenv_boolean("GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", default=True)
|
||||
|
||||
GRAFANA_INCIDENT_STATIC_API_KEY = os.environ.get("GRAFANA_INCIDENT_STATIC_API_KEY", None)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue