mirror of
https://github.com/GSA/notifications-api.git
synced 2026-09-08 17:53:12 -04:00
Added statsd integration into the API
- new client for statsd, follows conventions used elsewhere for configuration - client wraps underlying library so we can use a config property to send/not send statsd Added statsd metrics for: - count of API successful calls SMS/Email - count of successful task execution for SMS/Email - count of errors from Client libraries - timing of API calls to third party clients - timing of how long messages live on the SQS queue
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import statsd
|
||||||
from flask import request, url_for, g
|
from flask import request, url_for, g
|
||||||
from flask import Flask, _request_ctx_stack
|
from flask import Flask, _request_ctx_stack
|
||||||
from flask.ext.sqlalchemy import SQLAlchemy
|
from flask.ext.sqlalchemy import SQLAlchemy
|
||||||
@@ -10,10 +12,10 @@ from notifications_utils import logging
|
|||||||
from app.celery.celery import NotifyCelery
|
from app.celery.celery import NotifyCelery
|
||||||
from app.clients import Clients
|
from app.clients import Clients
|
||||||
from app.clients.sms.mmg import MMGClient
|
from app.clients.sms.mmg import MMGClient
|
||||||
from app.clients.sms.twilio import TwilioClient
|
|
||||||
from app.clients.sms.firetext import FiretextClient
|
from app.clients.sms.firetext import FiretextClient
|
||||||
from app.clients.sms.loadtesting import LoadtestingClient
|
from app.clients.sms.loadtesting import LoadtestingClient
|
||||||
from app.clients.email.aws_ses import AwsSesClient
|
from app.clients.email.aws_ses import AwsSesClient
|
||||||
|
from app.clients.statsd.statsd_client import StatsdClient
|
||||||
from app.encryption import Encryption
|
from app.encryption import Encryption
|
||||||
|
|
||||||
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
|
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
|
||||||
@@ -22,12 +24,12 @@ DATE_FORMAT = "%Y-%m-%d"
|
|||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
ma = Marshmallow()
|
ma = Marshmallow()
|
||||||
notify_celery = NotifyCelery()
|
notify_celery = NotifyCelery()
|
||||||
twilio_client = TwilioClient()
|
|
||||||
firetext_client = FiretextClient()
|
firetext_client = FiretextClient()
|
||||||
loadtest_client = LoadtestingClient()
|
loadtest_client = LoadtestingClient()
|
||||||
mmg_client = MMGClient()
|
mmg_client = MMGClient()
|
||||||
aws_ses_client = AwsSesClient()
|
aws_ses_client = AwsSesClient()
|
||||||
encryption = Encryption()
|
encryption = Encryption()
|
||||||
|
statsd_client = StatsdClient()
|
||||||
|
|
||||||
clients = Clients()
|
clients = Clients()
|
||||||
|
|
||||||
@@ -47,11 +49,11 @@ def create_app(app_name=None):
|
|||||||
ma.init_app(application)
|
ma.init_app(application)
|
||||||
init_app(application)
|
init_app(application)
|
||||||
logging.init_app(application)
|
logging.init_app(application)
|
||||||
twilio_client.init_app(application)
|
statsd_client.init_app(application)
|
||||||
firetext_client.init_app(application)
|
firetext_client.init_app(application, statsd_client=statsd_client)
|
||||||
loadtest_client.init_app(application)
|
loadtest_client.init_app(application, statsd_client=statsd_client)
|
||||||
mmg_client.init_app(application.config)
|
mmg_client.init_app(application.config, statsd_client=statsd_client)
|
||||||
aws_ses_client.init_app(application.config['AWS_REGION'])
|
aws_ses_client.init_app(application.config['AWS_REGION'], statsd_client=statsd_client)
|
||||||
notify_celery.init_app(application)
|
notify_celery.init_app(application)
|
||||||
encryption.init_app(application)
|
encryption.init_app(application)
|
||||||
clients.init_app(sms_clients=[firetext_client, mmg_client, loadtest_client], email_clients=[aws_ses_client])
|
clients.init_app(sms_clients=[firetext_client, mmg_client, loadtest_client], email_clients=[aws_ses_client])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from app import clients
|
from app import clients, statsd_client
|
||||||
from app.clients.email import EmailClientException
|
from app.clients.email import EmailClientException
|
||||||
from app.clients.sms import SmsClientException
|
from app.clients.sms import SmsClientException
|
||||||
from app.dao.services_dao import dao_fetch_service_by_id
|
from app.dao.services_dao import dao_fetch_service_by_id
|
||||||
@@ -190,6 +190,7 @@ def process_job(job_id):
|
|||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
"Job {} created at {} started at {} finished at {}".format(job_id, job.created_at, start, finished)
|
"Job {} created at {} started at {} finished at {}".format(job_id, job.created_at, start, finished)
|
||||||
)
|
)
|
||||||
|
statsd_client.incr("notifications.tasks.process-job")
|
||||||
|
|
||||||
|
|
||||||
@notify_celery.task(name="remove-job")
|
@notify_celery.task(name="remove-job")
|
||||||
@@ -235,7 +236,11 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
|
|||||||
sent_by=provider.get_name(),
|
sent_by=provider.get_name(),
|
||||||
content_char_count=template.replaced_content_count
|
content_char_count=template.replaced_content_count
|
||||||
)
|
)
|
||||||
|
statsd_client.timing_with_dates(
|
||||||
|
"notifications.tasks.send-sms.queued-for",
|
||||||
|
sent_at,
|
||||||
|
datetime.strptime(created_at, DATETIME_FORMAT)
|
||||||
|
)
|
||||||
dao_create_notification(notification_db_object, TEMPLATE_TYPE_SMS, provider.get_name())
|
dao_create_notification(notification_db_object, TEMPLATE_TYPE_SMS, provider.get_name())
|
||||||
|
|
||||||
if restricted:
|
if restricted:
|
||||||
@@ -259,6 +264,7 @@ def send_sms(service_id, notification_id, encrypted_notification, created_at):
|
|||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
"SMS {} created at {} sent at {}".format(notification_id, created_at, sent_at)
|
"SMS {} created at {} sent at {}".format(notification_id, created_at, sent_at)
|
||||||
)
|
)
|
||||||
|
statsd_client.incr("notifications.tasks.send-sms")
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
|
|
||||||
@@ -293,6 +299,11 @@ def send_email(service_id, notification_id, from_address, encrypted_notification
|
|||||||
)
|
)
|
||||||
|
|
||||||
dao_create_notification(notification_db_object, TEMPLATE_TYPE_EMAIL, provider.get_name())
|
dao_create_notification(notification_db_object, TEMPLATE_TYPE_EMAIL, provider.get_name())
|
||||||
|
statsd_client.timing_with_dates(
|
||||||
|
"notifications.tasks.send-email.queued-for",
|
||||||
|
sent_at,
|
||||||
|
datetime.strptime(created_at, DATETIME_FORMAT)
|
||||||
|
)
|
||||||
|
|
||||||
if restricted:
|
if restricted:
|
||||||
return
|
return
|
||||||
@@ -309,15 +320,18 @@ def send_email(service_id, notification_id, from_address, encrypted_notification
|
|||||||
body=template.replaced_govuk_escaped,
|
body=template.replaced_govuk_escaped,
|
||||||
html_body=template.as_HTML_email,
|
html_body=template.as_HTML_email,
|
||||||
)
|
)
|
||||||
|
|
||||||
update_notification_reference_by_id(notification_id, reference)
|
update_notification_reference_by_id(notification_id, reference)
|
||||||
|
|
||||||
except EmailClientException as e:
|
except EmailClientException as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
notification_db_object.status = 'failed'
|
notification_db_object.status = 'failed'
|
||||||
|
dao_update_notification(notification_db_object)
|
||||||
|
|
||||||
dao_update_notification(notification_db_object)
|
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
"Email {} created at {} sent at {}".format(notification_id, created_at, sent_at)
|
"Email {} created at {} sent at {}".format(notification_id, created_at, sent_at)
|
||||||
)
|
)
|
||||||
|
statsd_client.incr("notifications.tasks.send-email")
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ class AwsSesClient(EmailClient):
|
|||||||
Amazon SES email client.
|
Amazon SES email client.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def init_app(self, region, *args, **kwargs):
|
def init_app(self, region, statsd_client, *args, **kwargs):
|
||||||
self._client = boto3.client('ses', region_name=region)
|
self._client = boto3.client('ses', region_name=region)
|
||||||
super(AwsSesClient, self).__init__(*args, **kwargs)
|
super(AwsSesClient, self).__init__(*args, **kwargs)
|
||||||
self.name = 'ses'
|
self.name = 'ses'
|
||||||
|
self.statsd_client = statsd_client
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -88,7 +89,9 @@ class AwsSesClient(EmailClient):
|
|||||||
ReplyToAddresses=reply_to_addresses)
|
ReplyToAddresses=reply_to_addresses)
|
||||||
elapsed_time = monotonic() - start_time
|
elapsed_time = monotonic() - start_time
|
||||||
current_app.logger.info("AWS SES request finished in {}".format(elapsed_time))
|
current_app.logger.info("AWS SES request finished in {}".format(elapsed_time))
|
||||||
|
self.statsd_client.timing("notifications.clients.ses.request-time", elapsed_time)
|
||||||
return response['MessageId']
|
return response['MessageId']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO logging exceptions
|
# TODO logging exceptions
|
||||||
|
self.statsd_client.incr("notifications.clients.ses.error")
|
||||||
raise AwsSesClientException(str(e))
|
raise AwsSesClientException(str(e))
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ class FiretextClient(SmsClient):
|
|||||||
FireText sms client.
|
FireText sms client.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def init_app(self, config, *args, **kwargs):
|
def init_app(self, config, statsd_client, *args, **kwargs):
|
||||||
super(SmsClient, self).__init__(*args, **kwargs)
|
super(SmsClient, self).__init__(*args, **kwargs)
|
||||||
self.api_key = config.config.get('FIRETEXT_API_KEY')
|
self.api_key = config.config.get('FIRETEXT_API_KEY')
|
||||||
self.from_number = config.config.get('FIRETEXT_NUMBER')
|
self.from_number = config.config.get('FIRETEXT_NUMBER')
|
||||||
self.name = 'firetext'
|
self.name = 'firetext'
|
||||||
|
self.statsd_client = statsd_client
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -90,8 +91,10 @@ class FiretextClient(SmsClient):
|
|||||||
api_error.message
|
api_error.message
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.statsd_client.incr("notifications.clients.firetext.error")
|
||||||
raise api_error
|
raise api_error
|
||||||
finally:
|
finally:
|
||||||
elapsed_time = monotonic() - start_time
|
elapsed_time = monotonic() - start_time
|
||||||
current_app.logger.info("Firetext request finished in {}".format(elapsed_time))
|
current_app.logger.info("Firetext request finished in {}".format(elapsed_time))
|
||||||
|
self.statsd_client.timing("notifications.clients.firetext.request-time", elapsed_time)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ class LoadtestingClient(FiretextClient):
|
|||||||
Loadtest sms client.
|
Loadtest sms client.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def init_app(self, config, *args, **kwargs):
|
def init_app(self, config, statsd_client, *args, **kwargs):
|
||||||
super(FiretextClient, self).__init__(*args, **kwargs)
|
super(FiretextClient, self).__init__(*args, **kwargs)
|
||||||
self.api_key = config.config.get('LOADTESTING_API_KEY')
|
self.api_key = config.config.get('LOADTESTING_API_KEY')
|
||||||
self.from_number = config.config.get('LOADTESTING_NUMBER')
|
self.from_number = config.config.get('LOADTESTING_NUMBER')
|
||||||
self.name = 'loadtesting'
|
self.name = 'loadtesting'
|
||||||
|
self.statsd_client = statsd_client
|
||||||
|
|||||||
@@ -39,11 +39,12 @@ class MMGClient(SmsClient):
|
|||||||
MMG sms client
|
MMG sms client
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def init_app(self, config, *args, **kwargs):
|
def init_app(self, config, statsd_client, *args, **kwargs):
|
||||||
super(SmsClient, self).__init__(*args, **kwargs)
|
super(SmsClient, self).__init__(*args, **kwargs)
|
||||||
self.api_key = config.get('MMG_API_KEY')
|
self.api_key = config.get('MMG_API_KEY')
|
||||||
self.from_number = config.get('MMG_FROM_NUMBER')
|
self.from_number = config.get('MMG_FROM_NUMBER')
|
||||||
self.name = 'mmg'
|
self.name = 'mmg'
|
||||||
|
self.statsd_client = statsd_client
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -78,8 +79,10 @@ class MMGClient(SmsClient):
|
|||||||
api_error.message
|
api_error.message
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.statsd_client.incr("notifications.clients.mmg.error")
|
||||||
raise api_error
|
raise api_error
|
||||||
finally:
|
finally:
|
||||||
elapsed_time = monotonic() - start_time
|
elapsed_time = monotonic() - start_time
|
||||||
|
self.statsd_client.timing("notifications.clients.mmg.request-time", elapsed_time)
|
||||||
current_app.logger.info("MMG request finished in {}".format(elapsed_time))
|
current_app.logger.info("MMG request finished in {}".format(elapsed_time))
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
from monotonic import monotonic
|
|
||||||
from app.clients.sms import (
|
|
||||||
SmsClient, SmsClientException)
|
|
||||||
from twilio.rest import TwilioRestClient
|
|
||||||
from twilio import TwilioRestException
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
|
|
||||||
class TwilioClientException(SmsClientException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TwilioClient(SmsClient):
|
|
||||||
'''
|
|
||||||
Twilio sms client.
|
|
||||||
'''
|
|
||||||
def init_app(self, config, *args, **kwargs):
|
|
||||||
super(TwilioClient, self).__init__(*args, **kwargs)
|
|
||||||
self.client = TwilioRestClient(
|
|
||||||
config.config.get('TWILIO_ACCOUNT_SID'),
|
|
||||||
config.config.get('TWILIO_AUTH_TOKEN'))
|
|
||||||
self.from_number = config.config.get('TWILIO_NUMBER')
|
|
||||||
self.name = 'twilio'
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def send_sms(self, to, content):
|
|
||||||
start_time = monotonic()
|
|
||||||
try:
|
|
||||||
response = self.client.messages.create(
|
|
||||||
body=content,
|
|
||||||
to=to,
|
|
||||||
from_=self.from_number
|
|
||||||
)
|
|
||||||
return response.sid
|
|
||||||
except TwilioRestException as e:
|
|
||||||
current_app.logger.exception(e)
|
|
||||||
raise TwilioClientException(e)
|
|
||||||
finally:
|
|
||||||
elapsed_time = monotonic() - start_time
|
|
||||||
current_app.logger.info("Twilio request finished in {}".format(elapsed_time))
|
|
||||||
|
|
||||||
def status(self, message_id):
|
|
||||||
try:
|
|
||||||
response = self.client.messages.get(message_id)
|
|
||||||
if response.status in ('delivered', 'failed'):
|
|
||||||
return response.status
|
|
||||||
elif response.status == 'undelivered':
|
|
||||||
return 'sending'
|
|
||||||
return None
|
|
||||||
except TwilioRestException as e:
|
|
||||||
current_app.logger.exception(e)
|
|
||||||
raise TwilioClientException(e)
|
|
||||||
0
app/clients/statsd/__init__.py
Normal file
0
app/clients/statsd/__init__.py
Normal file
26
app/clients/statsd/statsd_client.py
Normal file
26
app/clients/statsd/statsd_client.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from statsd import StatsClient
|
||||||
|
|
||||||
|
|
||||||
|
class StatsdClient(StatsClient):
|
||||||
|
def init_app(self, app, *args, **kwargs):
|
||||||
|
StatsClient.__init__(
|
||||||
|
self,
|
||||||
|
app.config.get('STATSD_HOST'),
|
||||||
|
app.config.get('STATSD_PORT'),
|
||||||
|
prefix=app.config.get('STATSD_PREFIX')
|
||||||
|
)
|
||||||
|
self.active = app.config.get('STATSD_ENABLED')
|
||||||
|
|
||||||
|
def incr(self, stat, count=1, rate=1):
|
||||||
|
if self.active:
|
||||||
|
super(StatsClient, self).incr(stat, count, rate)
|
||||||
|
|
||||||
|
def timing(self, stat, delta, rate=1):
|
||||||
|
if self.active:
|
||||||
|
super(StatsClient, self).timing(stat, delta, rate)
|
||||||
|
|
||||||
|
|
||||||
|
def timing_with_dates(self, stat, start, end, rate=1):
|
||||||
|
if self.active:
|
||||||
|
delta = (start - end).total_seconds() * 1000
|
||||||
|
super(StatsClient, self).timing(stat, delta, rate)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
from app import statsd_client
|
||||||
from app.dao import notifications_dao
|
from app.dao import notifications_dao
|
||||||
from app.clients.sms.firetext import get_firetext_responses
|
from app.clients.sms.firetext import get_firetext_responses
|
||||||
from app.clients.sms.mmg import get_mmg_responses
|
from app.clients.sms.mmg import get_mmg_responses
|
||||||
@@ -67,5 +69,6 @@ def process_sms_client_response(status, reference, client_name):
|
|||||||
reference,
|
reference,
|
||||||
notification_status_message))
|
notification_status_message))
|
||||||
|
|
||||||
|
statsd_client.incr('notifications.callback.{}.{}'.format(client_name.lower(), notification_statistics_status))
|
||||||
success = "{} callback succeeded. reference {} updated".format(client_name, reference)
|
success = "{} callback succeeded. reference {} updated".format(client_name, reference)
|
||||||
return success, errors
|
return success, errors
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import statsd
|
||||||
import itertools
|
import itertools
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
@@ -12,7 +12,7 @@ from flask import (
|
|||||||
from notifications_utils.recipients import allowed_to_send_to, first_column_heading
|
from notifications_utils.recipients import allowed_to_send_to, first_column_heading
|
||||||
from notifications_utils.template import Template
|
from notifications_utils.template import Template
|
||||||
from app.clients.email.aws_ses import get_aws_responses
|
from app.clients.email.aws_ses import get_aws_responses
|
||||||
from app import api_user, encryption, create_uuid, DATETIME_FORMAT, DATE_FORMAT
|
from app import api_user, encryption, create_uuid, DATETIME_FORMAT, DATE_FORMAT, statsd_client
|
||||||
from app.authentication.auth import require_admin
|
from app.authentication.auth import require_admin
|
||||||
from app.dao import (
|
from app.dao import (
|
||||||
templates_dao,
|
templates_dao,
|
||||||
@@ -103,6 +103,7 @@ def process_ses_response():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
statsd_client.incr('notifications.callback.ses.{}'.format(notification_statistics_status))
|
||||||
return jsonify(
|
return jsonify(
|
||||||
result="success", message="SES callback succeeded"
|
result="success", message="SES callback succeeded"
|
||||||
), 200
|
), 200
|
||||||
@@ -374,4 +375,5 @@ def send_notification(notification_type):
|
|||||||
datetime.utcnow().strftime(DATETIME_FORMAT)
|
datetime.utcnow().strftime(DATETIME_FORMAT)
|
||||||
), queue='email')
|
), queue='email')
|
||||||
|
|
||||||
|
statsd_client.incr('notifications.api.{}'.format(notification_type))
|
||||||
return jsonify(data={"notification": {"id": notification_id}}), 201
|
return jsonify(data={"notification": {"id": notification_id}}), 201
|
||||||
|
|||||||
10
config.py
10
config.py
@@ -87,12 +87,20 @@ class Config(object):
|
|||||||
CSV_UPLOAD_BUCKET_NAME = 'local-notifications-csv-upload'
|
CSV_UPLOAD_BUCKET_NAME = 'local-notifications-csv-upload'
|
||||||
NOTIFICATIONS_ALERT = 5 # five mins
|
NOTIFICATIONS_ALERT = 5 # five mins
|
||||||
|
|
||||||
|
STATSD_ENABLED = False
|
||||||
|
STATSD_HOST = "localhost"
|
||||||
|
STATSD_PORT = None
|
||||||
|
STATSD_PREFIX = None
|
||||||
|
|
||||||
|
|
||||||
class Development(Config):
|
class Development(Config):
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
MMG_API_KEY = os.environ['MMG_API_KEY']
|
MMG_API_KEY = os.environ['MMG_API_KEY']
|
||||||
CSV_UPLOAD_BUCKET_NAME = 'development-notifications-csv-upload'
|
CSV_UPLOAD_BUCKET_NAME = 'development-notifications-csv-upload'
|
||||||
|
STATSD_ENABLED = True
|
||||||
|
STATSD_HOST = os.getenv('STATSD_HOST')
|
||||||
|
STATSD_PORT = os.getenv('STATSD_PORT')
|
||||||
|
STATSD_PREFIX = os.getenv('STATSD_PREFIX')
|
||||||
|
|
||||||
class Preview(Config):
|
class Preview(Config):
|
||||||
MMG_API_KEY = os.environ['MMG_API_KEY']
|
MMG_API_KEY = os.environ['MMG_API_KEY']
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class Live(Config):
|
|||||||
TWILIO_AUTH_TOKEN = os.getenv('LIVE_TWILIO_AUTH_TOKEN')
|
TWILIO_AUTH_TOKEN = os.getenv('LIVE_TWILIO_AUTH_TOKEN')
|
||||||
MMG_API_KEY = os.environ['LIVE_MMG_API_KEY']
|
MMG_API_KEY = os.environ['LIVE_MMG_API_KEY']
|
||||||
CSV_UPLOAD_BUCKET_NAME = 'live-notifications-csv-upload'
|
CSV_UPLOAD_BUCKET_NAME = 'live-notifications-csv-upload'
|
||||||
|
STATSD_ENABLED = True
|
||||||
|
STATSD_HOST = os.getenv('LIVE_STATSD_HOST')
|
||||||
|
STATSD_PORT = os.getenv('LIVE_STATSD_PORT')
|
||||||
|
STATSD_PREFIX = os.getenv('LIVE_STATSD_PREFIX')
|
||||||
|
|
||||||
BROKER_TRANSPORT_OPTIONS = {
|
BROKER_TRANSPORT_OPTIONS = {
|
||||||
'region': 'eu-west-1',
|
'region': 'eu-west-1',
|
||||||
|
|||||||
@@ -22,3 +22,7 @@ export MMG_API_KEY='mmg-secret-key'
|
|||||||
export MMG_FROM_NUMBER='test'
|
export MMG_FROM_NUMBER='test'
|
||||||
export LOADTESTING_API_KEY="loadtesting"
|
export LOADTESTING_API_KEY="loadtesting"
|
||||||
export LOADTESTING_NUMBER="loadtesting"
|
export LOADTESTING_NUMBER="loadtesting"
|
||||||
|
export STATSD_ENABLED=True
|
||||||
|
export STATSD_HOST="somehost"
|
||||||
|
export STATSD_PORT=1000
|
||||||
|
export STATSD_PREFIX="stats-prefix"
|
||||||
@@ -17,7 +17,7 @@ boto==2.39.0
|
|||||||
celery==3.1.20
|
celery==3.1.20
|
||||||
twilio==4.6.0
|
twilio==4.6.0
|
||||||
monotonic==0.3
|
monotonic==0.3
|
||||||
|
statsd==3.2.1
|
||||||
|
|
||||||
git+https://github.com/alphagov/notifications-python-client.git@1.0.0#egg=notifications-python-client==1.0.0
|
git+https://github.com/alphagov/notifications-python-client.git@1.0.0#egg=notifications-python-client==1.0.0
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from app.celery.tasks import (
|
|||||||
delete_successful_notifications,
|
delete_successful_notifications,
|
||||||
provider_to_use
|
provider_to_use
|
||||||
)
|
)
|
||||||
from app import (aws_ses_client, encryption, DATETIME_FORMAT, mmg_client)
|
from app import (aws_ses_client, encryption, DATETIME_FORMAT, mmg_client, statsd_client)
|
||||||
from app.clients.email.aws_ses import AwsSesClientException
|
from app.clients.email.aws_ses import AwsSesClientException
|
||||||
from app.clients.sms.mmg import MMGClientException
|
from app.clients.sms.mmg import MMGClientException
|
||||||
from app.dao import notifications_dao, jobs_dao, provider_details_dao
|
from app.dao import notifications_dao, jobs_dao, provider_details_dao
|
||||||
@@ -103,6 +103,7 @@ def test_should_call_delete_invotations_on_delete_invitations_task(notify_api, m
|
|||||||
|
|
||||||
@freeze_time("2016-01-01 11:09:00.061258")
|
@freeze_time("2016-01-01 11:09:00.061258")
|
||||||
def test_should_process_sms_job(sample_job, mocker, mock_celery_remove_job):
|
def test_should_process_sms_job(sample_job, mocker, mock_celery_remove_job):
|
||||||
|
mocker.patch('app.statsd_client.incr')
|
||||||
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('sms'))
|
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=load_example_csv('sms'))
|
||||||
mocker.patch('app.celery.tasks.send_sms.apply_async')
|
mocker.patch('app.celery.tasks.send_sms.apply_async')
|
||||||
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
||||||
@@ -124,6 +125,7 @@ def test_should_process_sms_job(sample_job, mocker, mock_celery_remove_job):
|
|||||||
)
|
)
|
||||||
job = jobs_dao.dao_get_job_by_id(sample_job.id)
|
job = jobs_dao.dao_get_job_by_id(sample_job.id)
|
||||||
assert job.status == 'finished'
|
assert job.status == 'finished'
|
||||||
|
statsd_client.incr.assert_called_once_with("notifications.tasks.process-job")
|
||||||
|
|
||||||
|
|
||||||
@freeze_time("2016-01-01 11:09:00.061258")
|
@freeze_time("2016-01-01 11:09:00.061258")
|
||||||
@@ -329,21 +331,37 @@ def test_should_send_template_to_correct_sms_provider_and_persist(sample_templat
|
|||||||
mocker.patch('app.encryption.decrypt', return_value=notification)
|
mocker.patch('app.encryption.decrypt', return_value=notification)
|
||||||
mocker.patch('app.mmg_client.send_sms')
|
mocker.patch('app.mmg_client.send_sms')
|
||||||
mocker.patch('app.mmg_client.get_name', return_value="mmg")
|
mocker.patch('app.mmg_client.get_name', return_value="mmg")
|
||||||
|
mocker.patch('app.statsd_client.incr')
|
||||||
|
mocker.patch('app.statsd_client.timing_with_dates')
|
||||||
|
|
||||||
notification_id = uuid.uuid4()
|
notification_id = uuid.uuid4()
|
||||||
|
|
||||||
|
freezer = freeze_time("2016-01-01 11:09:00.00000")
|
||||||
|
freezer.start()
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
|
freezer.stop()
|
||||||
|
|
||||||
|
freezer = freeze_time("2016-01-01 11:10:00.00000")
|
||||||
|
freezer.start()
|
||||||
|
|
||||||
send_sms(
|
send_sms(
|
||||||
sample_template_with_placeholders.service_id,
|
sample_template_with_placeholders.service_id,
|
||||||
notification_id,
|
notification_id,
|
||||||
"encrypted-in-reality",
|
"encrypted-in-reality",
|
||||||
now.strftime(DATETIME_FORMAT)
|
now.strftime(DATETIME_FORMAT)
|
||||||
)
|
)
|
||||||
|
freezer.stop()
|
||||||
|
|
||||||
|
statsd_client.timing_with_dates.assert_called_once_with(
|
||||||
|
"notifications.tasks.send-sms.queued-for", datetime(2016, 1, 1, 11, 10, 0, 00000), datetime(2016, 1, 1, 11, 9, 0, 00000)
|
||||||
|
)
|
||||||
|
|
||||||
mmg_client.send_sms.assert_called_once_with(
|
mmg_client.send_sms.assert_called_once_with(
|
||||||
to=format_phone_number(validate_phone_number("+447234123123")),
|
to=format_phone_number(validate_phone_number("+447234123123")),
|
||||||
content="Sample service: Hello Jo",
|
content="Sample service: Hello Jo",
|
||||||
reference=str(notification_id)
|
reference=str(notification_id)
|
||||||
)
|
)
|
||||||
|
statsd_client.incr.assert_called_once_with("notifications.tasks.send-sms")
|
||||||
persisted_notification = notifications_dao.get_notification(
|
persisted_notification = notifications_dao.get_notification(
|
||||||
sample_template_with_placeholders.service_id, notification_id
|
sample_template_with_placeholders.service_id, notification_id
|
||||||
)
|
)
|
||||||
@@ -539,11 +557,21 @@ def test_should_use_email_template_and_persist(sample_email_template_with_placeh
|
|||||||
"personalisation": {"name": "Jo"}
|
"personalisation": {"name": "Jo"}
|
||||||
}
|
}
|
||||||
mocker.patch('app.encryption.decrypt', return_value=notification)
|
mocker.patch('app.encryption.decrypt', return_value=notification)
|
||||||
mocker.patch('app.aws_ses_client.send_email')
|
mocker.patch('app.statsd_client.incr')
|
||||||
|
mocker.patch('app.statsd_client.timing_with_dates')
|
||||||
mocker.patch('app.aws_ses_client.get_name', return_value='ses')
|
mocker.patch('app.aws_ses_client.get_name', return_value='ses')
|
||||||
|
mocker.patch('app.aws_ses_client.send_email', return_value='ses')
|
||||||
|
|
||||||
notification_id = uuid.uuid4()
|
notification_id = uuid.uuid4()
|
||||||
|
|
||||||
|
freezer = freeze_time("2016-01-01 11:09:00.00000")
|
||||||
|
freezer.start()
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
|
freezer.stop()
|
||||||
|
|
||||||
|
freezer = freeze_time("2016-01-01 11:10:00.00000")
|
||||||
|
freezer.start()
|
||||||
|
|
||||||
send_email(
|
send_email(
|
||||||
sample_email_template_with_placeholders.service_id,
|
sample_email_template_with_placeholders.service_id,
|
||||||
notification_id,
|
notification_id,
|
||||||
@@ -551,6 +579,8 @@ def test_should_use_email_template_and_persist(sample_email_template_with_placeh
|
|||||||
"encrypted-in-reality",
|
"encrypted-in-reality",
|
||||||
now.strftime(DATETIME_FORMAT)
|
now.strftime(DATETIME_FORMAT)
|
||||||
)
|
)
|
||||||
|
freezer.stop()
|
||||||
|
|
||||||
aws_ses_client.send_email.assert_called_once_with(
|
aws_ses_client.send_email.assert_called_once_with(
|
||||||
"email_from",
|
"email_from",
|
||||||
"my_email@my_email.com",
|
"my_email@my_email.com",
|
||||||
@@ -558,9 +588,16 @@ def test_should_use_email_template_and_persist(sample_email_template_with_placeh
|
|||||||
body="Hello Jo",
|
body="Hello Jo",
|
||||||
html_body=AnyStringWith("Hello Jo")
|
html_body=AnyStringWith("Hello Jo")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
statsd_client.incr.assert_called_once_with("notifications.tasks.send-email")
|
||||||
|
statsd_client.timing_with_dates.assert_called_once_with(
|
||||||
|
"notifications.tasks.send-email.queued-for", datetime(2016, 1, 1, 11, 10, 0, 00000), datetime(2016, 1, 1, 11, 9, 0, 00000)
|
||||||
|
)
|
||||||
|
|
||||||
persisted_notification = notifications_dao.get_notification(
|
persisted_notification = notifications_dao.get_notification(
|
||||||
sample_email_template_with_placeholders.service_id, notification_id
|
sample_email_template_with_placeholders.service_id, notification_id
|
||||||
)
|
)
|
||||||
|
|
||||||
assert persisted_notification.id == notification_id
|
assert persisted_notification.id == notification_id
|
||||||
assert persisted_notification.to == 'my_email@my_email.com'
|
assert persisted_notification.to == 'my_email@my_email.com'
|
||||||
assert persisted_notification.template_id == sample_email_template_with_placeholders.id
|
assert persisted_notification.template_id == sample_email_template_with_placeholders.id
|
||||||
|
|||||||
@@ -1510,6 +1510,110 @@ def test_should_handle_validation_code_callbacks(notify_api, notify_db, notify_d
|
|||||||
assert json_resp['message'] == 'SES callback succeeded'
|
assert json_resp['message'] == 'SES callback succeeded'
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_record_email_request_in_statsd(notify_api, notify_db, notify_db_session, sample_email_template, mocker):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
with notify_api.test_client() as client:
|
||||||
|
mocker.patch('app.statsd_client.incr')
|
||||||
|
mocker.patch('app.celery.tasks.send_email.apply_async')
|
||||||
|
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'to': 'ok@ok.com',
|
||||||
|
'template': str(sample_email_template.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header = create_authorization_header(service_id=sample_email_template.service_id)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
path='/notifications/email',
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=[('Content-Type', 'application/json'), auth_header])
|
||||||
|
assert response.status_code == 201
|
||||||
|
app.statsd_client.incr.assert_called_once_with("notification.api.email")
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_record_sms_request_in_statsd(notify_api, notify_db, notify_db_session, sample_template, mocker):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
with notify_api.test_client() as client:
|
||||||
|
mocker.patch('app.statsd_client.incr')
|
||||||
|
mocker.patch('app.celery.tasks.send_sms.apply_async')
|
||||||
|
mocker.patch('app.encryption.encrypt', return_value="something_encrypted")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'to': '07123123123',
|
||||||
|
'template': str(sample_template.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_header = create_authorization_header(service_id=sample_template.service_id)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
path='/notifications/sms',
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=[('Content-Type', 'application/json'), auth_header])
|
||||||
|
assert response.status_code == 201
|
||||||
|
app.statsd_client.incr.assert_called_once_with("notification.api.sms")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ses_callback_should_update_record_statsd(
|
||||||
|
notify_api,
|
||||||
|
notify_db,
|
||||||
|
notify_db_session,
|
||||||
|
sample_email_template,
|
||||||
|
mocker):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
with notify_api.test_client() as client:
|
||||||
|
mocker.patch('app.statsd_client.incr')
|
||||||
|
|
||||||
|
notification = create_sample_notification(
|
||||||
|
notify_db,
|
||||||
|
notify_db_session,
|
||||||
|
template=sample_email_template,
|
||||||
|
reference='ref'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert get_notification_by_id(notification.id).status == 'sending'
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
path='/notifications/email/ses',
|
||||||
|
data=ses_notification_callback(),
|
||||||
|
headers=[('Content-Type', 'text/plain; charset=UTF-8')]
|
||||||
|
)
|
||||||
|
app.statsd_client.incr.assert_called_once_with("notifications.callback.ses.delivered")
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_mmg_response_records_statsd(notify_api, sample_notification, mocker):
|
||||||
|
with notify_api.test_client() as client:
|
||||||
|
mocker.patch('app.statsd_client.incr')
|
||||||
|
data = json.dumps({"reference": "mmg_reference",
|
||||||
|
"CID": str(sample_notification.id),
|
||||||
|
"MSISDN": "447777349060",
|
||||||
|
"status": "3",
|
||||||
|
"deliverytime": "2016-04-05 16:01:07"})
|
||||||
|
|
||||||
|
client.post(path='notifications/sms/mmg',
|
||||||
|
data=data,
|
||||||
|
headers=[('Content-Type', 'application/json')])
|
||||||
|
app.statsd_client.incr.assert_called_once_with("notifications.callback.mmg.delivered")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_firetext_callback_should_record_statsd(notify_api, notify_db, notify_db_session, mocker):
|
||||||
|
with notify_api.test_request_context():
|
||||||
|
with notify_api.test_client() as client:
|
||||||
|
mocker.patch('app.statsd_client.incr')
|
||||||
|
notification = create_sample_notification(notify_db, notify_db_session, status='delivered')
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
path='/notifications/sms/firetext',
|
||||||
|
data='mobile=441234123123&status=2&time=2016-03-10 14:17:00&reference={}'.format(
|
||||||
|
notification.id
|
||||||
|
),
|
||||||
|
headers=[('Content-Type', 'application/x-www-form-urlencoded')])
|
||||||
|
|
||||||
|
app.statsd_client.incr.assert_called_once_with("notifications.callback.firetext.delivered")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def ses_validation_code_callback():
|
def ses_validation_code_callback():
|
||||||
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"valid-code@test.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"messageId\\":\\"ref\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.u\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
|
return b'{\n "Type" : "Notification",\n "MessageId" : "ref",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Delivery\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"valid-code@test.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"messageId\\":\\"ref\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.u\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user