Files
notifications-api/app/clients/sms/mmg.py
David McDonald ac6837cde5 Downgrade exception to warning for provider API call
When we send an HTTP request to our SMS providers, there is a
chance we get a 5xx status code back from them. Currently we log this as
two different exception level logs.

If a provider has a funny few minutes, we could end up with
hundreds of exceptions thrown and pagerduty waking someone up in the
middle of the night. These problems tend to pretty quickly fix
themselves as we balance traffic from one SMS to the other SMS provider
within 5 minutes.

By downgrading both exceptions to warning in the case of a
`SmsClientResponseException`, we will reduce the change of waking us up
in the middle of the night for no reason.

If the error is not a `SmsClientResponseException`, then we will still
log at the exception level as before as this is more unexpected and we
may want to be alerted sooner.

What we still want to happen though is that let's say both SMS providers
went down at the same time for 1 hour. We don't want our tasks to just
sit there, retrying every 5 minutes for the whole time without us being
aware (so we can at least raise a statuspage update). Luckily we will
still be alerted because our smoke tests will fail after 10 minutes and
raise a p1:
https://github.com/alphagov/notifications-functional-tests/blob/master/tests/functional/staging_and_prod/notify_api/test_notify_api_sms.py#L21
2021-01-18 17:00:21 +00:00

141 lines
5.1 KiB
Python

import json
from time import monotonic
from requests import (request, RequestException)
from app.clients.sms import (SmsClient, SmsClientResponseException)
mmg_response_map = {
'2': {'status': 'permanent-failure', 'substatus': {
"1": "Number does not exist",
"4": "Rejected by operator",
"5": "Unidentified Subscriber",
"9": "Undelivered",
"11": "Service for Subscriber suspended",
"12": "Illegal equipment",
"2049": "Subscriber IMSI blacklisted",
"2050": "Number blacklisted in do-not-disturb blacklist",
"2052": "Destination number blacklisted",
"2053": "Source address blacklisted"
}},
'3': {'status': 'delivered', 'substatus': {"2": "Delivered to operator", "5": "Delivered to handset"}},
'4': {'status': 'temporary-failure', 'substatus': {
"6": "Absent Subscriber",
"8": "Roaming not allowed",
"13": "SMS Not Supported",
"15": "Expired",
"27": "Absent Subscriber",
"29": "Invalid delivery report",
"32": "Delivery Failure",
}},
'5': {'status': 'permanent-failure', 'substatus': {
"6": "Network out of coverage",
"8": "Incorrect number prefix",
"10": "Number on do-not-disturb service",
"11": "Sender id not registered",
"13": "Sender id blacklisted",
"14": "Destination number blacklisted",
"19": "Routing unavailable",
"20": "Rejected by anti-flooding mechanism",
"21": "System error", # it says to retry those messages or contact support
"23": "Duplicate message id",
"24": "Message formatted incorrectly",
"25": "Message too long",
"51": "Missing recipient value",
"52": "Invalid destination",
}},
}
def get_mmg_responses(status, detailed_status_code=None):
return (mmg_response_map[status]["status"], mmg_response_map[status]["substatus"].get(detailed_status_code, None))
class MMGClientResponseException(SmsClientResponseException):
def __init__(self, response, exception):
status_code = response.status_code if response is not None else 504
text = response.text if response is not None else "Gateway Time-out"
self.status_code = status_code
self.text = text
self.exception = exception
def __str__(self):
return "Code {} text {} exception {}".format(self.status_code, self.text, str(self.exception))
class MMGClient(SmsClient):
'''
MMG sms client
'''
def init_app(self, current_app, statsd_client, *args, **kwargs):
super(SmsClient, self).__init__(*args, **kwargs)
self.current_app = current_app
self.api_key = current_app.config.get('MMG_API_KEY')
self.from_number = current_app.config.get('FROM_NUMBER')
self.name = 'mmg'
self.statsd_client = statsd_client
self.mmg_url = current_app.config.get('MMG_URL')
def record_outcome(self, success, response):
status_code = response.status_code if response else 503
log_message = "API {} request {} on {} response status_code {}".format(
"POST",
"succeeded" if success else "failed",
self.mmg_url,
status_code
)
if success:
self.current_app.logger.info(log_message)
self.statsd_client.incr("clients.mmg.success")
else:
self.statsd_client.incr("clients.mmg.error")
self.current_app.logger.warning(log_message)
def get_name(self):
return self.name
def send_sms(self, to, content, reference, multi=True, sender=None):
data = {
"reqType": "BULK",
"MSISDN": to,
"msg": content,
"sender": self.from_number if sender is None else sender,
"cid": reference,
"multi": multi
}
response = None
start_time = monotonic()
try:
response = request(
"POST",
self.mmg_url,
data=json.dumps(data),
headers={
'Content-Type': 'application/json',
'Authorization': 'Basic {}'.format(self.api_key)
},
timeout=60
)
response.raise_for_status()
try:
json.loads(response.text)
except (ValueError, AttributeError) as e:
self.record_outcome(False, response)
raise MMGClientResponseException(response=response, exception=e)
self.record_outcome(True, response)
except RequestException as e:
self.record_outcome(False, e.response)
raise MMGClientResponseException(response=e.response, exception=e)
finally:
elapsed_time = monotonic() - start_time
self.statsd_client.timing("clients.mmg.request-time", elapsed_time)
if response and hasattr(response, 'elapsed'):
self.statsd_client.timing("clients.mmg.raw-request-time", response.elapsed.total_seconds())
self.current_app.logger.info("MMG request for {} finished in {}".format(reference, elapsed_time))
return response