DEV Community

Cover image for Extending Python Logger for mailing Exceptions
satyamsoni2211
satyamsoni2211

Posted on

Extending Python Logger for mailing Exceptions

Logging is most crucial part of any application or process you create, as it helps you debug or keep track of whats going on. Logging is a very vast topic in itself but super useful when you want to perform some side effects or redirect your output to some other services or perform some side computations etc.

Highly configurable by design, we can add extend functionalities of existing loggers, using custom Handlers.

We will try to extend functionality of existing logger to e-mail exceptions occurred during code execution. Let us create a python Logger using built-in library logging.

import logging
logger: logging.Logger = logging.getLogger()
Enter fullscreen mode Exit fullscreen mode

Above code will return a root logger instance, since we are not mentioning any name explicitly. If you want a named logger instance, you can pass name to getLogger function.

Let us now create a custom Handler to handle records and perform some side effect. We can inherit from logging.Handler abstract base class to create our custom Handler.

logging.Handler base class provides multiple hooks that you can override. We will override emit hook for our requirement.

import logging
class MailHandler(logging.Handler):

    def emit(self, record: logging.LogRecord) -> None:
        if record.exc_info:
            exception = "".join(traceback.format_exception(*record.exc_info))
        else:
            exception = "".join(traceback.format_exception(*sys.exc_info()))
        self._send_mail(exception)
Enter fullscreen mode Exit fullscreen mode

We have added set of statements to intercept exception from the record and create formatted stack trace.

emit hook will receive logging.LogRecord which will contain all the details regarding the record like message, timestamp, line no, exception info etc. We have also added a instance method _send_mail to send formatted stack trace to the user.

Let us now complete _send_mail function. We will use AWS SES for sending e-mail. You may also use smtp as an alternative.

import boto3

class MailHandler(logging.Handler):

    def _send_mail(self, message):
        self.client = boto3.client('ses')
        ses_arn = os.getenv('SES_ARN')
        source = os.getenv('SES_SOURCE')
        html = f"""
        <p>Exception occurred while execution. Please check. </p>
        <pre>{message}</pre>
        """
        self.client.send_email(
            Source=source,
            Destination={
                'ToAddresses': [
                    'foobar@gmail.com',
                ],
            },
            Message={
                'Subject': {
                    'Data': 'Exception occurred while executing Lambda. Please check.',
                },
                'Body': {
                    'Html': {
                        'Data': html
                    }
                }
            },
            SourceArn=ses_arn
        )
Enter fullscreen mode Exit fullscreen mode

We are using boto3 library for connecting to SES and send e-mail.

I am reading ses_arn and source from environment variables. These will be required to send e-mail to the destination address using your configured SES record.

We are done with creating our custom handler. Let us register this with our logger instance.

import logging
logger: logging.Logger = logging.getLogger()
handler = MailHandler()
handler.setLevel(logging.ERROR)
logger.logger_.addHandler(handler)
Enter fullscreen mode Exit fullscreen mode

We have registered our custom handler with our logger instance. It will be only activated on error record type as we have set level to logging.ERROR. You may now test your custom handler as below.

logger.error(Exception("Fake Exception"))
Enter fullscreen mode Exit fullscreen mode

You should receive an email, with exception and stack trace.

Below is the complete code for custom handler.

import boto3
import logging

class MailHandler(logging.Handler):
    def emit(self, record: logging.LogRecord) -> None:
        if record.exc_info:
            exception = "".join(traceback.format_exception(*record.exc_info))
        else:
            exception = "".join(traceback.format_exception(*sys.exc_info()))
        self._send_mail(exception)

    def _send_mail(self, message):
        self.client = boto3.client('ses')
        ses_arn = os.getenv('SES_ARN')
        source = os.getenv('SES_SOURCE')
        html = f"""
        <p>Exception occurred while execution. Please check. </p>
        <pre>{message}</pre>
        """
        self.client.send_email(
            Source=source,
            Destination={
                'ToAddresses': [
                    'foobar@gmail.com',
                ],
            },
            Message={
                'Subject': {
                    'Data': 'Exception occurred while executing Lambda. Please check.',
                },
                'Body': {
                    'Html': {
                        'Data': html
                    }
                }
            },
            SourceArn=ses_arn
        )

logger: logging.Logger = logging.getLogger()
handler = MailHandler()
handler.setLevel(logging.ERROR)
logger.logger_.addHandler(handler)

#raising exception
try:
    raise Exception("Fake Exception")
except Exception as e:
    logger.error(e, exc_info=True)
Enter fullscreen mode Exit fullscreen mode

You can now customize logging handlers has per your requirements.

Happy Logging 😊.

Top comments (0)