This content originally appeared on Twilio Blog and was authored by Renato Byrro
Error detection is a key part of any application deployed in the cloud. No matter how much care we give to testing and software quality, there will always be factors - sometimes outside our control - that can make our applications fail.
In this article, we will build a native Python solution that extends the standard logging library to send application failure alerts through Twilio Programmable SMS and/or SendGrid Email APIs.
Requirements
We will be using Python 3.9, but any version of Python from 3.6 should work as well. You can download Python from the official python.org website.
Since our code will use Twilio and SendGrid services to dispatch error alerts, you will need an account in at least one of these services (ideally both). We provide instructions below on how to sign-up or log-in and also collect the data you will need from each account.
Twilio Account
Login to your Twilio account (or sign up for free if you don’t have one) and take note of your Account SID and Auth Token:
Make sure you have a phone number listed in Phone Numbers > Manage Numbers and that the number has messaging enabled. If you don’t have a number, click on the “Buy a number” button at the upper-right corner of the screen to get one.
SendGrid Account
Login to your SendGrid account (or sign up if you don’t have one) and go to Settings > API Keys in the left menu. Click “Create API Key” in the top-right corner. Give the key a name and hit “Create & View”. Take note of the key, since SendGrid won’t display it to you again.
Project set up
Create a directory for the project:
mkdir twilio-alert
cd twilio-alert
Creating a virtual environment is often good practice, so let’s get this done now:
python3 -m venv .env
source .env/bin/activate
On a Windows computer, replace the source
command in the last line above with:
.venv\Scripts\activate
You should now see a (.env)
prefix added to your prompt, which confirms that your virtual environment is fully set up.
Install the following libraries on your virtual environment:
pip install http-logging sendgrid twilio
Optionally, pin the installed libraries to a local dependencies file:
pip freeze > requirements.txt
The http-logging library is compatible with the native logging library from the Python standard library. It allows us to implement a custom backend to receive error messages, which in our case will send the errors to the Twilio and SendGrid APIs. The async logging handler in this library is similar to Python’s HTTP Handler class, but instead of generating blocking requests, it runs in a background thread to avoid blocking the main program, and also sends logs in batches, can keep a local cache in SQLite, and handles retries in case of remote API failure.
Environment Variables
We will use environment variables to retrieve secret API keys needed by the Async HTTP Logger to communicate with the Twilio and SendGrid backends.
Again, if you would like to use only one of the two services, skip the environment variables related to the other. For example: if you only want to use SMS, skip SENDGRID_SENDER_EMAIL
, SENDGRID_API_KEY
and ALERT_EMAIL
.
export TWILIO_ACCOUNT_SID="XXXXX"
export TWILIO_AUTH_TOKEN="XXXXX"
export TWILIO_SENDER_NUMBER="+1234567890"
export SENDGRID_SENDER_EMAIL="sent@from.com"
export SENDGRID_API_KEY="XXXXX"
export ALERT_PHONE="1234567890"
export ALERT_EMAIL="hello@world.com"
Linux and MacOS should support the export command. On Windows, if you use a command prompt, you should use set
instead of export
. In PowerShell console, use $Env
as follows:
$Env: TWILIO_ACCOUNT_SID="XXXXX"
$Env: TWILIO_AUTH_TOKEN="XXXXX"
$Env: TWILIO_SENDER_NUMBER="+1234567890"
$Env: SENDGRID_SENDER_EMAIL="sent@from.com"
$Env: SENDGRID_API_KEY="XXXXX"
$Env: ALERT_PHONE="1234567890"
$Env: ALERT_EMAIL="hello@world.com"
When setting phone numbers, make sure to enter the complete number in E.164 format, which includes the plus sign and the country code.
Twilio HTTP Transport
In this section we are going to write a custom HTTP Transport class that will be responsible for communicating with the Twilio and SendGrid APIs.
Create a new Python file called logging_twilio.py
. In this file, our custom class will inherit from http_logging.transport.AsyncHttpTransport
. Import all required libraries and declare a custom class as indicated below:
from typing import List, Optional
from http_logging.transport import AsyncHttpTransport
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
from twilio.rest import Client as TwilioClient
class TwilioHttpTransport(AsyncHttpTransport):
pass
We will use some custom attributes that are not part of the parent class implementation. For that, we need to override the constructor method in TwilioHttpTransport
:
class TwilioHttpTransport(AsyncHttpTransport):
def __init__(
self,
logger_name: str,
twilio_account_sid: Optional[str] = None,
twilio_auth_token: Optional[str] = None,
twilio_sender_number: Optional[str] = None,
sendgrid_sender_email: Optional[str] = None,
sendgrid_api_key: Optional[str] = None,
alert_phone: Optional[str] = None,
alert_email: Optional[List[str]] = None,
*args,
**kwargs,
) -> None:
self.logger_name = logger_name
self.alert_context = f'Alert from logger: {logger_name}'
self.twilio_account_sid = twilio_account_sid
self.twilio_auth_token = twilio_auth_token
self.twilio_sender_number = twilio_sender_number
self.sendgrid_sender_email = sendgrid_sender_email
self.sendgrid_api_key = sendgrid_api_key
self.alert_phone = alert_phone
self.alert_email = alert_email
super().__init__(*args, **kwargs)
To customize the class behavior, we need to override its parent send
method. It takes an events
argument, which will be a list of events logged by our Python apps.
class TwilioHttpTransport(AsyncHttpTransport):
# ...
def send(self, events: List[bytes], **kwargs) -> None:
batches = list(self._HttpTransport__batches(events))
if self.alert_phone:
self.send_sms_alert(batches=batches)
if self.alert_email:
self.send_email_alert(batches=batches)
Now let’s implement the send_sms_alert
and send_email_alert
methods, which will use the Twilio and SendGrid APIs, respectively:
def send_sms_alert(self, batches: List[dict]) -> None:
twilio_client = TwilioClient(
username=self.twilio_account_sid,
password=self.twilio_auth_token,
)
sms_logs = ', '.join([
f"{log['level']['name']}: {log['message']}"
for batch in batches
for log in batch
])
twilio_client.messages.create(
body=f'[{self.alert_context}] {sms_logs}',
from_=self.twilio_sender_number,
to=self.alert_phone,
)
def send_email_alert(self, batches: List[dict]) -> None:
msg = '<hr>'.join([
self.build_log_html(log)
for batch in batches
for log in batch
])
message = Mail(
from_email=self.sendgrid_sender_email,
to_emails=self.alert_email,
subject=self.alert_context,
html_content=msg,
)
sg = SendGridAPIClient(self.sendgrid_api_key)
response = sg.send(message)
def build_log_html(self, log):
return '<br>'.join([
f'<b>{key}:</b> {val}'
for key, val in log.items()
])
SMS stands for Short Message Service, so we obviously want to keep our alerts short on this channel. Additionally, the purpose is to only raise an alert, and not to provide all the details about the issue. Because of that, we concatenate multiple events (if there is more than one) in a single message, providing only the type of error and summary.
In the email channel, we get more verbose and include the error stack trace and some context information that could help to identify the route cause and fix the issue.
The logger name is sent in both channels. This way we can identify from which application the alerts are coming from.
Now that we have an HTTP Transport class integrated with Twilio and SendGrid, the next requirement is the logic to instantiate a Logger object that relies on the new TwilioHttpTransport
facility.
Twilio HTTP Handler
The HTTP Transport class is ready, but it requires a handler class to work properly with the native Python logging machinery. This should be an instance of the http_logging.AsyncHttpHandler
class.
Create a new file called sample_app.py
and enter the following code to Instantiate the Twilio HTTP transport and handler classes:
import logging
import os
from http_logging.handler import AsyncHttpHandler
from logging_twilio import TwilioHttpTransport
APP_NAME = 'MyApp'
transport_class = TwilioHttpTransport(
logger_name=APP_NAME,
twilio_account_sid=os.environ.get('TWILIO_ACCOUNT_SID'),
twilio_auth_token=os.environ.get('TWILIO_AUTH_TOKEN'),
twilio_sender_number=os.environ.get('TWILIO_SENDER_NUMBER'),
sendgrid_api_key=os.environ.get('SENDGRID_API_KEY'),
sendgrid_sender_email=os.environ.get('SENDGRID_SENDER_EMAIL'),
alert_phone=os.environ.get('ALERT_PHONE'),
alert_email=os.environ.get('ALERT_EMAIL'),
)
twilio_handler = AsyncHttpHandler(transport_class=transport_class)
After that, we instantiate a logging.Logger
object and add the twilio_handler
as its handler:
logger = logging.getLogger(APP_NAME)
logger.addHandler(twilio_handler)
If you already have a logger
object coming from a different package (such as app.logger
from the Flask framework, for example), skip the first line above and just use logger.addHandler(twilio_handler)
, where logger
is the Logger object you already have in your application.
Notice that the secrets, phone numbers and email addresses are being retrieved from the environment variables we set at the beginning of this tutorial. This provides flexibility in case we want to use this code in multiple projects. It also avoids hardcoding API secrets, which is not a good idea.
Multiple handlers
The Python logging package is very powerful. The logging.Logger
class is flexible enough to be extended with multiple handlers.
As explained above, the TwilioHttpTransport
class will send minimal information about logs due to SMS limitations. Nonetheless, in the event of an error that requires further debugging, we certainly will want to grab the entire stack trace, information about which line of code failed, exact timestamps, etc. Although this will be sent in email messages, it is also advisable to keep the information in the local logs.
We can accomplish that by using the Logger.addHandler
and adding one (or more) handlers to the Logger
object.
For example, to send logs not only to our phone and email address, but also to the console, we may use the logging.StreamHandler
, as demonstrated below:
logger = logging.getLogger(APP_NAME)
logger.addHandler(twilio_handler)
logger.addHandler(logging.StreamHandler())
Anything logged with the above logger
object will be printed to the console and sent to our phone and email address through the Twilio and SendGrid APIs.
A logging.FileHandler
may be used to store logs in the local filesystem if that makes sense. You could also use the same http_logging.AsyncHttpHandler
again, but in this case, sending logs to a different backend host apart from Twilio and SendGrid.
Testing With a Sample Application
To test our new async error alerting system, add the following lines at the end of the sample_app.py
script to trigger some log messages and an intentional error:
logger.debug('Debugging...')
logger.warning('You\'ve been warned!')
logger.error('This is a test error')
try:
1/0
except ArithmeticError as exc:
logger.exception(exc)
In the console, run this script with:
python sample_app.py
The following output should be printed to the console:
You've been warned!
This is a test error
division by zero
Traceback (most recent call last):
File "/home/twilio-alert/sample_app.py", line 14
1/0
ZeroDivisionError: division by zero
If you have everything set up correctly, you should receive SMS and email messages similar to the screenshots below. If you did not set up the options for one of the services (Twilio SMS or SendGrid Email) it will be ignored by the error alerting code.
Notice that the debug message 'Debugging...'
was not printed to the console nor concatenated into the SMS and Email messages. That is because the default log level in the Python logging library is WARNING
. The DEBUG
level is lower than WARNING
and, thus, discarded.
If you want DEBUG
level messages to also be captured, set the level accordingly as shown below:
logger.setLevel(logging.DEBUG)
Despite our logger
reliance on a custom handler (http_logging.AsyncHttpHandler
) and a custom Transport (logging_twilio.TwilioHttpTransport
) class, it behaves just like any other Python Logger
object.
This makes it fully compatible as a drop-in replacement for any Python project you currently have, in case you’d like to integrate the SMS and Email alerting mechanism we’ve just developed throughout your stack and in any future project.
Wrapping Up
We’re done with our simple, yet powerful, Python alerting tool using SMS and Email, powered by the Twilio and SendGrid APIs. Since it is based on the native Python logging facility, we can use the same Python API we’re used to and easily integrate it in any project. One advantage is that it has no fixed costs, we are only charged when an SMS or Email message is actually sent.
A cache of logs is stored locally by the http-logging library. In the event that the external APIs or the cell phone carrier experience a downtime or network instability, our logger will retry sending the SMS and Email alerts later.
I’m a backend software developer and a father of two amazing kids who won’t let me sleep so that I can enjoy spending nights connecting APIs around. Keep track of other projects fresh from dawn on my Github profile. My direct contact and other social links in byrro.dev.
This content originally appeared on Twilio Blog and was authored by Renato Byrro

Renato Byrro | Sciencx (2021-05-25T12:10:31+00:00) Python Error Alerting with Twilio and SendGrid. Retrieved from https://www.scien.cx/2021/05/25/python-error-alerting-with-twilio-and-sendgrid/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.