Skip to content

Commit 089b53e

Browse files
committed
Initial commit
0 parents  commit 089b53e

File tree

6 files changed

+203
-0
lines changed

6 files changed

+203
-0
lines changed

README

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## WHAT'S THIS
2+
3+
By default Django sends server errors by email which might be not convenient
4+
(you can't setup monitoring) and not always secure (a single bug in a hot place
5+
could overflow mail server). Django_errorlog enables logging of server errors
6+
with standard Python logging.
7+
8+
## USAGE
9+
10+
To install in a Django project:
11+
12+
1. Include 'django_errorlog' into INSTALLED_APPS
13+
14+
2. Setup handlers for log channels 'exception' and 'traceback' with usual
15+
Python [logging handlers][1]. It's a good idea to have a separate file (or
16+
whatever) for 'traceback' logger because its messages don't fit on a single
17+
line and hence not easily grep'able.
18+
19+
3. To disable default mailing of server errors you can leave ADMINS setting
20+
empty or not setup SMTP_HOST.
21+
22+
The application works automatically: it listents for a signal that Django sends
23+
on all uncaught server errors and then logs short exception values and full
24+
tracebacks into their respective log channels.
25+
26+
There are two utility functions in django_errorlog.utils: log_error and
27+
log_warning. They can be used to manually log exception that you do handle in
28+
your code. They accept exc_info (a triple of (exceptions, value, traceback) as
29+
an argument. If called without arguments they get it from sys.exc_info().
30+
31+
[1]: http://docs.python.org/library/logging.html#handlers

django_errorlog/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding:utf-8 -*-
2+
'''
3+
Приложение вешается на сигнал, который Django посылает в случае фатальных
4+
ошибок и логирует значения и полные traceback'и исключений в логгеры
5+
'exception' и 'traceback' соответственно.
6+
7+
Хендлеры для логгеров можно настраивать в проекте, а можно попросить
8+
django_errorlog создать дефолтные. Для этого надо импортировать к себе
9+
настройки из django_errorlog.settings и прописать свои имена файлов в
10+
EXCEPTION_LOG_FILE и TRACEBACK_LOG_FILE.
11+
12+
13+
Есть две вспомогательные функции в модуле utils: log_error и log_warning,
14+
которые нужны, если в общий лог ошибок надо слогировать exception вручную.
15+
Обе функции принимают либо exc_info, либо, если вызваны без параметров, берут
16+
его из sys.exc_info() - текущего обрабатываемого исключения.
17+
'''

django_errorlog/models.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# -*- coding:utf-8 -*-
2+
import sys
3+
import os
4+
import re
5+
import traceback
6+
import logging
7+
from logging import handlers
8+
9+
from django.core import signals
10+
from django.utils.encoding import smart_str
11+
from django.conf import settings
12+
13+
def _get_logger(name, setting_name):
14+
'''
15+
Returns a named logger.
16+
17+
Creates a default file handler for it if there's a setting for it
18+
(deprecated).
19+
'''
20+
if name not in _get_logger.loggers:
21+
logger = logging.getLogger(name)
22+
if getattr(settings, setting_name, ''):
23+
try:
24+
handler = handlers.RotatingFileHandler(
25+
getattr(settings, setting_name),
26+
'a',
27+
settings.LOGGING_MAX_FILE_SIZE,
28+
settings.LOGGING_MAX_FILES_COUNT
29+
)
30+
except:
31+
handler = logging.StreamHandler(None)
32+
handler.setFormatter(logging.Formatter(settings.LOGGING_FORMAT))
33+
logger.addHandler(handler)
34+
_get_logger.loggers[name] = logger
35+
36+
return _get_logger.loggers[name]
37+
_get_logger.loggers = {}
38+
39+
def exception_str(value):
40+
'''
41+
Formats Exception object to a string. Unlike default str():
42+
43+
- can handle unicode strings in exception arguments
44+
- tries to format arguments as str(), not as repr()
45+
'''
46+
try:
47+
return ', '.join([smart_str(b) for b in value])
48+
except (TypeError, AttributeError): # happens for non-iterable values
49+
try:
50+
return smart_str(value)
51+
except UnicodeEncodeError:
52+
try:
53+
return repr(value)
54+
except Exception:
55+
return '<Unprintable value>'
56+
57+
POST_TRUNCATE_SIZE = 1024
58+
59+
def format_post(request):
60+
'''
61+
Casts request post data to string value. Depending on content type it's
62+
either a dict or raw post data.
63+
'''
64+
if request.method == 'POST' and request.META['CONTENT_TYPE'] not in ['application/x-www-form-urlencoded', 'multipart/form-data']:
65+
value = request.raw_post_data[:POST_TRUNCATE_SIZE]
66+
if len(request.raw_post_data) > len(value):
67+
value += '... (post data truncated at %s bytes)' % POST_TRUNCATE_SIZE
68+
return value
69+
else:
70+
return str(request.POST)
71+
72+
def _log_exc_info(exc_info=None, level=logging.ERROR, aditional_lines=None):
73+
'''
74+
Logs exception info into 'exception' and 'traceback' loggers calling
75+
formatting as necessary.
76+
'''
77+
exception, value, tb = exc_info or sys.exc_info()
78+
exception_logger = _get_logger('exception', 'EXCEPTION_LOG_FILE')
79+
# find innermost call
80+
inner = tb
81+
while inner.tb_next:
82+
inner = inner.tb_next
83+
lineno = inner.tb_lineno
84+
module_name = inner.tb_frame.f_globals['__name__']
85+
exception_logger.log(level, '%-20s %s:%s %s' % (
86+
exception.__name__,
87+
module_name,
88+
lineno,
89+
exception_str(value),
90+
))
91+
92+
lines = traceback.format_exception(exception, value, tb)
93+
if aditional_lines:
94+
lines = aditional_lines + lines
95+
traceback_logger = _get_logger('traceback', 'TRACEBACK_LOG_FILE')
96+
traceback_logger.log(level, '\n'.join([smart_str(l) for l in lines]))
97+
98+
def _log_request_error(sender, request, **kwargs):
99+
'''
100+
Handles unhandled request exceptions.
101+
'''
102+
lines = [
103+
'Path: %s' % request.path,
104+
'GET: %s' % request.GET,
105+
'POST: %s' % format_post(request),
106+
]
107+
_log_exc_info(aditional_lines=lines)
108+
109+
signals.got_request_exception.connect(_log_request_error)
110+

django_errorlog/settings.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding:utf-8 -*-
2+
3+
## Settings for creating default handlers for loggers. This is deprecated
4+
## in favor of setting up handlers in a project manually.
5+
6+
# Filename for logging one-line exception values
7+
EXCEPTION_LOG_FILE = ''
8+
9+
# Filename for logging full tracebacks
10+
TRACEBACK_LOG_FILE = ''
11+
12+
# Log format
13+
LOGGING_FORMAT = '%(asctime)s %(name)-15s %(levelname)s %(message)s'
14+
15+
# Log file rotation settings
16+
LOGGING_MAX_FILE_SIZE = 1024*1024
17+
LOGGING_MAX_FILES_COUNT = 10

django_errorlog/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding:utf-8 -*-
2+
import logging
3+
4+
from django_errorlog.models import _log_exc_info, exception_str
5+
6+
def log_error(exc_info=None):
7+
'''
8+
Logs exc_info into 'exception' and 'traceback' logs with ERROR level.
9+
If exc_info is None get it from sys.exc_info().
10+
'''
11+
_log_exc_info(exc_info, level=logging.ERROR)
12+
13+
def log_warning(exc_info=None):
14+
'''
15+
Logs exc_info into 'exception' and 'traceback' logs with WARNING level.
16+
If exc_info is None get it from sys.exc_info().
17+
'''
18+
_log_exc_info(exc_info, level=logging.WARNING)
19+

setup.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from distutils.core import setup
2+
3+
setup(
4+
name='django_errorlog',
5+
description='Django application for logging server (aka "500") errors',
6+
packages=[
7+
'django_errorlog',
8+
],
9+
)

0 commit comments

Comments
 (0)