Skip to content

Instantly share code, notes, and snippets.

@DominikSerafin
Last active July 10, 2022 21:34
Show Gist options
  • Save DominikSerafin/0e14ea2ea60022201c9a0c04454a2926 to your computer and use it in GitHub Desktop.
Save DominikSerafin/0e14ea2ea60022201c9a0c04454a2926 to your computer and use it in GitHub Desktop.
Django Slack Logger
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',
}
@Speedy1991
Copy link

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

@DominikSerafin
Copy link
Author

DominikSerafin commented Jul 24, 2018

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

@chasetb
Copy link

chasetb commented Nov 9, 2018

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.

@aljiwala
Copy link

aljiwala commented Jun 5, 2019

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?

@op1490
Copy link

op1490 commented Jun 6, 2019

Third that - I am have issues with Django 2.2. SlackExceptionHandler is not getting invoked...

@op1490
Copy link

op1490 commented Jun 6, 2019

@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,
},
},
})

@chasetb
Copy link

chasetb commented Jun 25, 2019

@op1490 & @aljiwala I also had to overwrite the LOGGING dict to get it to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment