Logging Errors to Email in Python


Python has a fantastic logging module built in to the language that is incredibly powerful. It is super easy to setup multiple logging objects with differing settings for each python class or module.

Note: This post will combine some answers from a few stack overflow posts to try and centralize information. This is using python 2.6+ but should work the same in python 3.0+. I couldn't find an example of classes having their own specific logger to isolate bugs or configure formatting, just modules. I needed class objects to have different logging names to isolate bugs.

Specific Logs for Modules or Classes

When looking back at logs, sometimes it is important to understand where the log is coming from. For example, what module is this log from (You can tell this by the log's name).

Here we are going to create a class that will have call a class method calledsetupLogging() that will instantiate the class. You can create it in the __init__ method but this seems a bit too messy for me.

import logging

#(obj) Needed to access the class's name for python 2.6+
#Not needed in python 3+ I believe 
class SomeClass(obj):  
  def __init__(self):
    self.setupLogging()

  def setupLogging(self):
    logDir  = 'Logs/'
    logFile =  logDir + 'someLog.log'
    if not os.path.exists(logDir):
      os.makedirs(logDir)

    #create the file if it doesn't exist
    if not os.path.exists(logFile):
      with open(logFile, 'w+') as f:
        f.write('======== LOGGING FILE FOR SOMELOG ======\n')

    self.logger = logging.getLogger(type(self).__name__)
    self.logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter(
      '[%(name)-5.5s | %(levelname)-4.47s]'
            ' [%(threadName)-10.10s | Func: %(funcName)-10.10s | LN:%(lineno)-4.4d| %(asctime)-21s]'
            '  %(message)s')

    streamHandler = logging.StreamHandler()
    streamHandler.setFormatter(formatter)
    streamHandler.setLevel(logging.DEBUG)
    self.logger.addHandler(streamHandler)
    fileHandler = logging.FileHandler(logFile, mode='a', encoding=None, delay=False)
    fileHandler.setFormatter(formatter)
    fileHandler.setLevel(logging.ERROR)
    self.logger.addHandler(fileHandler)

The type(self).__name__ will output the name of the class. This is why if we are using python 2.6+, we need to add the (obj) superclass to our class so we can access these attributes.

So above we created a python logging object that will log to a file if there error is above a certain point, and log to the stream. The output would look like
[NAME | LEVEL] [ThreadName | funcName | LN:### | 2016-12-14 10:44:13,991] <MSG>.

import logging  
import logging.handlers  
import smtplib  
import threading

def smtpThreadHolder(mailhost, port, username, password, fromaddr, toaddrs, msg):  
  try:
    smtp = smtplib.SMTP(mailhost, port)
  except:
    logging.error("Trying to make smtp variable")
  if username:
    smtp.ehlo()# for tls add this line
    smtp.starttls()# for tls add this line
    smtp.ehlo()# for tls add this line
  smtp.login(username, password)
  smtp.sendmail(fromaddr, toaddrs, msg)
  smtp.quit()

class ThreadedTlsSMTPHandler(logging.handlers.SMTPHandler):  
  def emit(self, record):
    try:
      import string #for tls add this line
      try:
        from email.utils import formatdate
      except ImportError:
        formatdate = self.date_time

      port = self.mailport
      if not port:
      port = smtplib.SMTP_PORT
      msg = self.format(record)
      msg = ("From: %s\r\nTo"
            "%s\r\nSubject:"
            "%s\r\nDate: %s\r\n\r\n%s"
            %(self.fromaddr, string.join( self.toaddrs, ","), self.getSubject(record),formatdate(), msg)
      thread = threading.Thread(target = smtpThreadHolder, args = (self.mailhost, port, self.username, self.password, self.fromaddr, self.toaddrs, msg))
      thread.daemon = True
      thread.start()
    except(KeyboardInterrupt, SystemExit):
      raise
    except:
      self.handleError(record)    
#(obj) Needed to access the class's name for python 2.6+
#Not needed in python 3+ I believe 
class SomeClass(obj):  
  def __init__(self):
    self.setupLogging()

  def setupLogging(self):
    logDir  = 'Logs/'
    logFile =  logDir + 'someLog.log'
    if not os.path.exists(logDir):
      os.makedirs(logDir)

    #create the file if it doesn't exist
    if not os.path.exists(logFile):
      with open(logFile, 'w+') as f:
        f.write('======== LOGGING FILE FOR SOMELOG ======\n')

    self.logger = logging.getLogger(type(self).__name__)
    self.logger.setLevel(logging.DEBUG)
    formatter = logging.Formatter(
      '[%(name)-5.5s | %(levelname)-4.47s]'
            ' [%(threadName)-10.10s | Func: %(funcName)-10.10s | LN:%(lineno)-4.4d| %(asctime)-21s]'
            '  %(message)s')
    streamHandler = logging.StreamHandler()
    streamHandler.setFormatter(formatter)
    streamHandler.setLevel(logging.DEBUG)
    self.logger.addHandler(streamHandler)
    fileHandler = logging.FileHandler(logFile, mode='a', encoding=None, delay=False)
    fileHandler.setFormatter(formatter)
    fileHandler.setLevel(logging.ERROR)
    self.logger.addHandler(fileHandler)
    host = 'smtp.gmail.com'
    port = 587
    destEmails = 'List of send emails'
    fromEmail = 'FromEmail@mail.com'
    fromPass = 'FromPassword'
    gm = ThreadedTlsSMTPHandler(
        mailhost = (host, port),
        fromaddr = fromEmail,
        toaddrs = destEmails,
        subject = 'Error!',
        credentials = (
          fromEmail,
          fromPass
        )
    )
    gm.setLevel(logging.ERROR)
    gm.setFormatter(formatter)
    self.logger.addHandler(gm)
    #Send yourself an email with the body Test
    self.logger.error("TEST")
Press ` to check out my sick terminal!