Skip to content

Instantly share code, notes, and snippets.

@ninapavlich
Created December 20, 2018 23:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ninapavlich/032aad690f22d972634255e05cbab734 to your computer and use it in GitHub Desktop.
Save ninapavlich/032aad690f22d972634255e05cbab734 to your computer and use it in GitHub Desktop.
Django Slack Error Logger using File Uploads
# 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)
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