Created
December 20, 2018 23:32
-
-
Save ninapavlich/032aad690f22d972634255e05cbab734 to your computer and use it in GitHub Desktop.
Django Slack Error Logger using File Uploads
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
# My favorite logging settings | |
# ----------------------------------------------------------------------------- | |
# LOGGING | |
# ----------------------------------------------------------------------------- | |
LOGGING = { | |
'version': 1, | |
'disable_existing_loggers': False, | |
'propagate': 1, | |
'filters': { | |
'require_debug_false': { | |
'()': 'django.utils.log.RequireDebugFalse', | |
}, | |
'require_debug_true': { | |
'()': 'django.utils.log.RequireDebugTrue', | |
}, | |
'require_not_local_environment': { | |
'()': 'path.to.slack_logging.RequireNotLocalEnvironment', | |
}, | |
'require_prod_environment': { | |
'()': 'path.to.slack_logging.RequireProdEnvironment', | |
}, | |
}, | |
'formatters': { | |
'verbose': { | |
'format': '%(asctime)19s [%(levelname)s] %(process)d:%(pathname)s:%(funcName)s():%(lineno)d - %(message)s', | |
'datefmt': '%Y-%m-%d %H:%M:%S' | |
}, | |
}, | |
'handlers': { | |
'console': { | |
'level': 'WARNING', | |
'class': 'logging.StreamHandler', | |
'formatter': 'verbose' | |
}, | |
'file': { | |
'level': 'INFO', | |
'class': 'logging.handlers.RotatingFileHandler', | |
'filename': env.get('LOG_FILE', os.path.join(VAR_ROOT, 'django.log')), | |
'maxBytes': 1024 * 1024 * 5, # 5 MB | |
'backupCount': 5, | |
'formatter': 'verbose', | |
'filters': ['require_prod_environment'], | |
}, | |
'slack_admins': { | |
'level': 'ERROR', | |
'filters': ['require_not_local_environment'], | |
'class': 'path.to.slack_logging.SlackExceptionHandler', | |
} | |
}, | |
'loggers': { | |
'django': { | |
'handlers': ['console', 'slack_admins', 'file'], | |
'level': 'WARNING', | |
'propagate': True, | |
}, | |
'root': { | |
'handlers': ['console', 'slack_admins', 'file'], | |
'level': 'WARNING', | |
} | |
} | |
} | |
# For Slack error notifications | |
SLACK_LOGGER_CHANNEL = env.get('SLACK_LOGGER_CHANNEL', None) | |
SLACK_LOGGER_TOKEN = env.get('SLACK_LOGGER_TOKEN', None) |
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
import logging | |
import requests | |
import traceback | |
from copy import copy | |
import uuid | |
from django.conf import settings | |
from django.utils.log import AdminEmailHandler | |
from django.views.debug import ExceptionReporter | |
from django.template import Context | |
from django.template import Template | |
error_file_template = """{{subject}} | |
Request: {{request.method|safe}}: {{request.build_absolute_uri|safe}} | |
User: {{user|safe}} | |
GET Params: | |
{{request.GET|safe}} | |
POST Data: | |
{{request.POST|safe}} | |
Message: | |
{{message|safe}} | |
UA: {{request.META.HTTP_USER_AGENT|safe}} | |
""" | |
error_comment_template = """{{emoji}} *{{subject}}* | |
{% if request %}*Request:* {{request.method|safe}}: {{request.build_absolute_uri|safe}}{% endif %} | |
{% if user %}*User:* {{user|safe}}{% endif %} | |
*Stack Trace:* {{stacktrace|safe}} | |
""" | |
emoji_error_levels = { | |
'critical': ':fire:', | |
'error': ':boom:', | |
'warning': ':face_with_head_bandage:' | |
} | |
class SlackExceptionHandler(AdminEmailHandler): | |
# replacing default django emit | |
# (https://github.com/django/django/blob/master/django/utils/log.py) | |
def emit(self, record, *args, **kwargs): | |
if settings.SLACK_LOGGER_TOKEN == None: | |
print("settings.SLACK_LOGGER_TOKEN is None") | |
return | |
if settings.SLACK_LOGGER_CHANNEL == None: | |
print("settings.SLACK_LOGGER_CHANNEL is None") | |
return | |
# original AdminEmailHandler "emit" method code (but without actually | |
# sending email) | |
try: | |
request = record.request | |
subject = '%s (%s IP): %s' % ( | |
record.levelname, | |
('internal' if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS | |
else 'EXTERNAL'), | |
record.getMessage() | |
) | |
except Exception: | |
subject = '%s: %s' % ( | |
record.levelname, | |
record.getMessage() | |
) | |
request = None | |
subject = self.format_subject(subject) | |
stacktrace = traceback.format_exc() | |
# Since we add a nicely formatted traceback on our own, create a copy | |
# of the log record without the exception data. | |
no_exc_record = copy(record) | |
no_exc_record.exc_info = None | |
no_exc_record.exc_text = None | |
if record.exc_info: | |
exc_info = record.exc_info | |
else: | |
exc_info = (None, record.getMessage(), None) | |
reporter = ExceptionReporter(request, is_email=True, *exc_info) | |
message = "%s\n\n%s" % (self.format( | |
no_exc_record), reporter.get_traceback_text()) | |
html_message = reporter.get_traceback_html() if self.include_html else None | |
user = None | |
try: | |
if request.user and request.user.is_authenticated: | |
user = request.user.username + \ | |
' (' + str(request.user.pk) + ')' | |
except: | |
user = None | |
emoji = '' | |
if record.levelname.lower() in emoji_error_levels: | |
emoji = emoji_error_levels[record.levelname.lower()] | |
filename = "%s.txt" % (str(uuid.uuid4())) | |
# Store entire logging info as a file in Slack: | |
template = Template(error_file_template) | |
context = Context({ | |
'subject': subject, | |
'request': request, | |
'message': message, | |
'record': record, | |
'user': user, | |
'stacktrace': stacktrace | |
}) | |
full_error_message = template.render(context) | |
template = Template(error_comment_template) | |
context = Context({ | |
'emoji': emoji, | |
'subject': subject, | |
'request': request, | |
'message': message, | |
'record': record, | |
'user': user, | |
'stacktrace': stacktrace | |
}) | |
error_comment = template.render(context) | |
data = { | |
'token': settings.SLACK_LOGGER_TOKEN, | |
'content': full_error_message, | |
'filetype': 'txt', | |
'filename': filename, | |
'title': subject, | |
'initial_comment': error_comment, | |
'channels': settings.SLACK_LOGGER_CHANNEL | |
} | |
file_url = None | |
try: | |
r = requests.post("https://slack.com/api/files.upload", data=data) | |
r.raise_for_status() | |
try: | |
file_url = r.json()['file']['url_private'] | |
except KeyError: | |
print("Cound not find url_private in output: %s" % (r.json())) | |
except requests.exceptions.HTTPError as errh: | |
print("Http Error while post error:", errh) | |
except requests.exceptions.ConnectionError as errc: | |
print("Error Connecting while post error:", errc) | |
except requests.exceptions.Timeout as errt: | |
print("Timeout Error while post error:", errt) | |
except requests.exceptions.RequestException as err: | |
print("Exception while post error:", err) | |
class RequireNotLocalEnvironment(logging.Filter): | |
def filter(self, record): | |
return ('local' not in settings.ENVIRONMENT) | |
class RequireProdEnvironment(logging.Filter): | |
def filter(self, record): | |
return ('prod' in settings.ENVIRONMENT) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment