5
votes

I wanted to make my Django app send debugging information to slack instead of by email, which is the default.

  • Disabling email is simple. Just don't put any emails in ADMINS setting
  • Sending information to slack is easy. Just add an incoming webhook

Now, where should i create the logic that sends the messge? Middleware seems to be a pretty good idea. I would do something like

class ExceptionMiddleware:
    def process_exception(self, request, exception):
        pretty_debugging_message = ...
        requests.post("https://my-slack-url/", {...})

The middleware would just return None so as to not interfere with the rest of the system; after all, the emailing has already been disabled.

So my question is: How do I get all the debugging info goodness that django collects? I can do something like

import sys, traceback
pretty_debugging_message = '\n'.join(
    traceback.format_exception(*sys.exc_info())
)

But this only provides the traceback. What about all of the locals, the session, the clients IP etc.?

Reading through https://docs.djangoproject.com/en/1.8/howto/error-reporting/, I get the idea that all of that information is not collected until after middleware is handled, that is, once everything is tried, and an error has not been handled, then Django runs its ErrorReporter and logs the information. Could I intervene with that process somehow and make it send the info to slack? Would that be better?

Update

My solution:

class SlackHandler(AdminEmailHandler):
    def emit(self, record):
        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()
            )
            filter = get_exception_reporter_filter(request)
            request_repr = '\n{0}'.format(filter.get_request_repr(request))
        except Exception:
            subject = '%s: %s' % (
                record.levelname,
                record.getMessage()
            )
            request = None
            request_repr = "unavailable"
        subject = self.format_subject(subject)

        if record.exc_info:
            exc_info = record.exc_info
        else:
            exc_info = (None, record.getMessage(), None)

        message = "%s\n\nRequest repr(): %s" % (self.format(record), request_repr)
        reporter = ExceptionReporter(request, is_email=True, *exc_info)
        html_message = reporter.get_traceback_html() if self.include_html else None

        requests.post(settings.SLACK_WEBHOOK_URL, json={
            "fallback": message,
            "pretext": "An error occured",
            "color": "#ef2a2a",
            "fields": [
                {
                    "title": "Error",
                    "value": message,
                    "short": False
                }
            ]
        })

In settings.py:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse'
        }
    },
    'handlers': {
        'slack': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'myapp.myapp.SlackHandler'
        }
        # 'mail_admins': {
        #     'level': 'ERROR',
        #     'filters': ['require_debug_false'],
        #     'class': 'django.utils.log.AdminEmailHandler'
        # }
    },
    'loggers': {
        'django.request': {
            'handlers': ['slack'],
            'level': 'ERROR',
            'propagate': True,
        },
    }
}
3
I've published an article about it. serafin.io/slack-django-errorsDominik Serafin

3 Answers

11
votes

I would advise you to create a custom logging handler. You can have a look at the AdminEmailHandler implementation and make your own or, even simpler, if that suits your needs, subclass it and only override the send_mail method.

import requests

from django.utils.log import AdminEmailHandler


class SlackHandler(AdminEmailHandler):
    def send_mail(self, subject, message, *args, **kwargs):
        html_message = kwargs.get('html_message')
        requests.post("https://my-slack-url/", {...})

You then need to configure the LOGGING setting to use your new handler instead of AdminEmailHandler. Here is Django's default logging configuration.

Example:

'handlers': {
    'slack': {
        'level': 'ERROR',
        'filters': ['require_debug_false'],
        'class': 'import.path.to.SlackHandler'
    }
},
# ...
'loggers': {
    'django.request': {
        'handlers': ['slack'],
        'level': 'ERROR',
        'propagate': False,
    },
    # ...
}
0
votes

My best solution to this issue is

class ExceptionMiddleware:
    def process_exception(self, request, exception):
        reporter = ExceptionReporter(
            request,
            *sys.exc_info()
        )
        html = reporter.get_traceback_html()
        plain = reporter..get_traceback_text()
        requests.post("https://my-slack-url/", {...})

It seems to be the way that django retrieves all the data. It also allows for getting and HTML and a plaintext version, so there are some options of how to display it.

0
votes

This works only on 500 errors:

  import requests
  from django.views.debug import ExceptionReporter

  SLACK_TEAM_NAME = "myteam"

  class SlackHandler(AdminEmailHandler):
    def emit(self, record):
        ### Copy the emit from AdminEmailHandler
        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()
            )
            filter = get_exception_reporter_filter(request)
            request_repr = '\n{}'.format(force_text(filter.get_request_repr(request)))
        except Exception:
            subject = '%s: %s' % (
                record.levelname,
                record.getMessage()
            )
            request = None
            request_repr = "unavailable"
        subject = self.format_subject(subject)

        if record.exc_info:
            exc_info = record.exc_info
        else:
            exc_info = (None, record.getMessage(), None)

        message = "%s\n\nRequest repr(): %s" % (self.format(record), request_repr)
        reporter = ExceptionReporter(request, is_email=True, *exc_info)

        ### my stuff here
        html_message = reporter.get_traceback_text()
        token = "XXXXXXXXXXXXXXXX"
        channel = "#XXXXXXXXXXXX"
        html_channel = channel.replace("#","%23")
        url = "https://%s.slack.com/services/hooks/slackbot?token=%s&channel=%s" % (SLACK_TEAM_NAME, token, html_channel)
        #print url
        r = requests.post(url, data=html_message)

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        }
    },
    'handlers': {
        'slack': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'myapp.settings.SlackHandler',
        },
    },
    'loggers': {
        'django.request': {
            'handlers': ['slack'],
            'level': 'ERROR',
            'propagate': False,
        },
    },
}