-
-
Save DominikSerafin/0e14ea2ea60022201c9a0c04454a2926 to your computer and use it in GitHub Desktop.
| This Gist supplements article available at https://serafin.io/article/slack-django-errors | |
| import requests | |
| import json | |
| import time | |
| import math | |
| from copy import copy | |
| from django.conf import settings | |
| from django.utils.log import AdminEmailHandler | |
| from django.views.debug import ExceptionReporter | |
| class SlackExceptionHandler(AdminEmailHandler): | |
| # replacing default django emit (https://github.com/django/django/blob/master/django/utils/log.py) | |
| def emit(self, record, *args, **kwargs): | |
| # 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) | |
| # 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 | |
| #self.send_mail(subject, message, fail_silently=True, html_message=html_message) | |
| # this is where original "emit" method code ends | |
| # construct slack attachment detail fields | |
| attachments = [ | |
| { | |
| 'title': subject, | |
| 'color': 'danger', | |
| 'fields': [ | |
| { | |
| "title": "Level", | |
| "value": record.levelname, | |
| "short": True, | |
| }, | |
| { | |
| "title": "Method", | |
| "value": request.method if request else 'No Request', | |
| "short": True, | |
| }, | |
| { | |
| "title": "Path", | |
| "value": request.path if request else 'No Request', | |
| "short": True, | |
| }, | |
| { | |
| "title": "User", | |
| "value": ( (request.user.username + ' (' + str(request.user.pk) + ')' | |
| if request.user.is_authenticated else 'Anonymous' ) | |
| if request else 'No Request' ), | |
| "short": True, | |
| }, | |
| { | |
| "title": "Status Code", | |
| "value": record.status_code, | |
| "short": True, | |
| }, | |
| { | |
| "title": "UA", | |
| "value": ( request.META['HTTP_USER_AGENT'] | |
| if request and request.META else 'No Request' ), | |
| "short": False, | |
| }, | |
| { | |
| "title": 'GET Params', | |
| "value": json.dumps(request.GET) if request else 'No Request', | |
| "short": False, | |
| }, | |
| { | |
| "title": "POST Data", | |
| "value": json.dumps(request.POST) if request else 'No Request', | |
| "short": False, | |
| }, | |
| ], | |
| }, | |
| ] | |
| # add main error message body | |
| # slack message attachment text has max of 8000 bytes | |
| # lets split it up into 7900 bytes long chunks to be on the safe side | |
| split = 7900 | |
| parts = range( math.ceil( len( message.encode('utf8') ) / split ) ) | |
| for part in parts: | |
| start = 0 if part == 0 else split * part | |
| end = split if part == 0 else split * part + split | |
| # combine final text and prepend it with line breaks | |
| # so the details in slack message will fully collapse | |
| detail_text = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n' + message[start:end] | |
| attachments.append({ | |
| 'color': 'danger', | |
| 'title': 'Details (Part {})'.format(part + 1), | |
| 'text': detail_text, | |
| 'ts': time.time(), | |
| }) | |
| # construct main text | |
| main_text = 'Error at ' + time.strftime("%A, %d %b %Y %H:%M:%S +0000", time.gmtime()) | |
| # construct data | |
| data = { | |
| 'payload': json.dumps({'main_text': main_text,'attachments': attachments}), | |
| } | |
| # setup channel webhook | |
| webhook_url = 'https://hooks.slack.com/services/xxx/xxx/xxx' | |
| # send it | |
| r = requests.post(webhook_url, data=data) | |
| from django.utils.log import DEFAULT_LOGGING | |
| LOGGING = DEFAULT_LOGGING | |
| LOGGING['handlers']['slack_admins'] = { | |
| 'level': 'ERROR', | |
| 'filters': ['require_debug_false'], | |
| 'class': 'config.helpers.slack_logger.SlackExceptionHandler', | |
| } | |
| LOGGING['loggers']['django'] = { | |
| 'handlers': ['console', 'slack_admins'], | |
| 'level': 'INFO', | |
| } |
Hey, @Speedy1991 I'm not 100% sure it will work (since it was long time ago when I wrote this), but I subclassed AdminEmailHandler and used its emit method from Django standard email reporting and Django sensitive information filtering should work in this logger just as in email reporting - https://docs.djangoproject.com/en/1.11/howto/error-reporting/#filtering-sensitive-information
Do you find that this is still working with Django 2.1? I was running a modified version of this successfully until upgrading to 2.1.
RE: https://gist.github.com/DominikSerafin/0e14ea2ea60022201c9a0c04454a2926#gistcomment-2755644
@chasetb
I think I'm having the same issue, as in, somehow the SlackExceptionHandler is not getting invoked/called.
Did you find any workaround for Django version > 2.1?
Third that - I am have issues with Django 2.2. SlackExceptionHandler is not getting invoked...
@aljiwala I have made this work by changing the LOGGING dict in settings:
import logging.config
LOGGING_CONFIG = None
logging.config.dictConfig({
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'console': {
# exact format is not important, this is the minimum information
'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'console',
},
# Add Handler for Slack for warning and above
'slack': {
'level': 'WARNING',
'class': 'your_file_path.slack_logger.SlackExceptionHandler',
},
},
'loggers': {
'': {
'level': 'WARNING',
'handlers': ['console', 'slack'],
},
'app_name': {
'level': 'INFO',
'handlers': ['console', 'slack'],
# required to avoid double logging with root logger
'propagate': False,
},
},
})
This line
"value": json.dumps(request.POST) if request else 'No Request',https://gist.github.com/DominikSerafin/0e14ea2ea60022201c9a0c04454a2926#file-a-slack_logger-py-L100
is realy dangerous. E.g. password filled forms will be exposed to Slack in cleartext