Created
July 1, 2018 12:35
-
-
Save Vimkxi/fcf0d00df0bee7f1831109289aee0df2 to your computer and use it in GitHub Desktop.
A Python Exception mail tracker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
""" | |
A Python Exception mail tracker. | |
https://www.vincentmazeas.com/blog/python-exception-tracker/ | |
""" | |
import sys | |
import smtplib | |
import traceback | |
from email.mime.text import MIMEText | |
from email.mime.multipart import MIMEMultipart | |
# Uncomment this line if you're using the tracker in Maya | |
# from maya import utils | |
class ExceptionTracker(object): | |
""" | |
Send a email with detailed information when an exception occur. | |
Use: | |
>>> ExceptionTracker.activate() | |
>>> ... | |
>>> ExceptionTracker.deactivate() | |
""" | |
@classmethod | |
def activate(cls): | |
"""Activate the tracker.""" | |
# Switch commented/uncommented if you're using the tracker in Maya | |
sys.excepthook = cls._mail | |
# utils.formatGuiException = cls._main | |
@classmethod | |
def deactivate(cls): | |
"""Deactivate the tracker.""" | |
# Switch commented/uncommented if you're using the tracker in Maya | |
sys.excepthook = sys.__excepthook__ | |
# utils.formatGuiException = utils._formatGuiException | |
# Main sys hook replacement | |
@classmethod | |
def _main(cls, exception_type, exception_value, traceback_): | |
"""Python exception hook that mail exception reports.""" | |
# Build mail contents | |
mail_contents = "\n".join(( | |
"<DIV>", | |
"<H3>Traceback</H3>", | |
cls._format_traceback(exception_type, exception_value, traceback_), | |
"<H3>Variables</H3>", | |
cls._format_variables(traceback_), | |
"</DIV>" | |
)) | |
# Prepare the mail | |
subject = "{}: {}".format(exception_type.__name__, exception_value) | |
your_address = "you@users.com" | |
user_address = "user@users.com" | |
msg = MIMEMultipart("alternative") | |
msg["Subject"] = subject | |
msg["From"] = user_address | |
msg["To"] = your_address | |
msg.attach(MIMEText(mail_contents, "html")) | |
# Send the message - you'll need to enter your own server here | |
server = smtplib.SMTP("smtp-server.client.com") | |
server.sendmail(user_address, your_address, msg.as_string()) | |
server.quit() | |
# Run the basic python error to print the traceback to the console | |
sys.__excepthook__(exception_type, exception_value, traceback_) | |
# Private formatters | |
@classmethod | |
def _format_traceback(cls, exception_type, exception_value, traceback_): | |
"""Format the call's traceback with proper indentation.""" | |
# Get call stack text | |
call_stack = cls._get_traceback_string( | |
exception_type, exception_value, traceback_ | |
) | |
html = [] | |
traceback_lines = call_stack.split("\n") | |
for line in traceback_lines: | |
# Keep only lines with content | |
line = line.strip() | |
if not line: | |
continue | |
# Apply offsets to preserve indentation, depending on the line | |
if line == traceback_lines[0] or line == traceback_lines[-1]: | |
html.append(' <SPAN>{}</SPAN>'.format(line)) | |
elif line.startswith('File "'): | |
html.append(' <SPAN style="margin-left: 15px">{}</SPAN>'.format(line)) | |
else: | |
html.append(' <SPAN style="margin-left: 30px">{}</SPAN>'.format(line)) | |
# Return traceback lines wrapped in a div with monospace font | |
html = "<br />\n".join(html) | |
return '<DIV style="font-family: monospace;">\n{}\n</DIV>\n'.format(html) | |
@classmethod | |
def _format_variables(cls, traceback_): | |
"""Return a html table of the local variables names and values.""" | |
# Build table with two columns: key/name & value | |
table = '<TABLE>\n' | |
for key, value in sorted(cls._get_exception_variables(traceback_).items()): | |
table += ' <TR>\n' | |
table += ' <TD>{} :</TD>\n'.format(key) | |
# Replacing "<" by "<" to prevent it from being turned into an HTML tag | |
v = str(value).replace('<', '<') | |
table += ' <TD>{}</TD>\n'.format(v) | |
table += ' </TR>\n' | |
table += '</TABLE>\n' | |
return table | |
# Private collectors | |
@staticmethod | |
def _get_traceback_string(exception_type, exception_value, traceback_): | |
"""Return the formatted traceback as a string.""" | |
return '\n'.join( | |
traceback.format_exception( | |
exception_type, exception_value, traceback_ | |
) | |
) | |
@staticmethod | |
def _get_exception_variables(traceback_): | |
"""Return a dict of variables {name: value} from traceback_.""" | |
# Get latest call stack frame (latest_frame is None with SyntaxErrors) | |
latest_frame = traceback_ | |
while latest_frame and latest_frame.tb_next: | |
latest_frame = latest_frame.tb_next | |
# No latest_frame means a SyntaxError which bears no vars | |
if latest_frame: | |
return latest_frame.tb_frame.f_locals | |
return [] | |
# Static class, no need to instantiate | |
def __init__(self): | |
raise NotImplementedError("ExceptionTracker is a static class.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment