From a604183c5b5db7adce7152d4e3833b8ae6a8161c Mon Sep 17 00:00:00 2001 From: Luke Faraone Date: Thu, 6 Dec 2012 16:00:34 -0500 Subject: [PATCH] Log errors to Humbug, too. (imported from commit 2547625135568f3ea004bf4287471a82bc0a4f38) --- humbug/ratelimit.py | 17 ++++++++---- humbug/settings.py | 24 +++++++++++------ zephyr/handlers.py | 66 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 zephyr/handlers.py diff --git a/humbug/ratelimit.py b/humbug/ratelimit.py index aca1cf3632..0f3291d3db 100644 --- a/humbug/ratelimit.py +++ b/humbug/ratelimit.py @@ -4,8 +4,7 @@ from datetime import datetime, timedelta # Adapted http://djangosnippets.org/snippets/2242/ by user s29 (October 25, 2010) -class RateLimitFilter(object): - +class _RateLimitFilter(object): last_error = 0 def filter(self, record): @@ -14,7 +13,8 @@ class RateLimitFilter(object): # Track duplicate errors duplicate = False - rate = getattr(settings, 'ERROR_RATE_LIMIT', 600) # seconds + rate = getattr(settings, '%s_LIMIT' % self.__class__.__name__.upper(), + 600) # seconds if rate > 0: # Test if the cache works try: @@ -24,8 +24,9 @@ class RateLimitFilter(object): use_cache = False if use_cache: - duplicate = cache.get('ERROR_RATE') == 1 - cache.set('ERROR_RATE', 1, rate) + key = self.__class__.__name__.upper() + duplicate = cache.get(key) == 1 + cache.set(key, 1, rate) else: min_date = datetime.now() - timedelta(seconds=rate) duplicate = (self.last_error >= min_date) @@ -33,3 +34,9 @@ class RateLimitFilter(object): self.last_error = datetime.now() return not duplicate + +class HumbugLimiter(_RateLimitFilter): + pass + +class EmailLimiter(_RateLimitFilter): + pass diff --git a/humbug/settings.py b/humbug/settings.py index 1cf94c2f03..5ca4dc1dcf 100644 --- a/humbug/settings.py +++ b/humbug/settings.py @@ -154,7 +154,6 @@ if deployed: 'LOCATION': '127.0.0.1:11211', 'TIMEOUT': 3600 } } - error_filters = ['ratelimit'] else: CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', @@ -164,9 +163,6 @@ else: 'MAX_ENTRIES': 100000 } } } - error_filters = [] - -ERROR_RATE_LIMIT=600 LOGGING = { 'version': 1, @@ -177,11 +173,23 @@ LOGGING = { } }, 'filters': { - 'ratelimit': { - '()': 'humbug.ratelimit.RateLimitFilter', + 'HumbugLimiter': { + '()': 'humbug.ratelimit.HumbugLimiter', + }, + 'EmailLimiter': { + '()': 'humbug.ratelimit.EmailLimiter', + }, + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse', } }, 'handlers': { + 'inapp': { + 'level': 'ERROR', + 'class': 'zephyr.handlers.AdminHumbugHandler', + 'filters': ['HumbugLimiter', 'require_debug_false'], + 'formatter': 'default' + }, 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', @@ -196,12 +204,12 @@ LOGGING = { 'mail_admins': { 'level': 'ERROR', 'class': 'django.utils.log.AdminEmailHandler', - 'filters': error_filters, + 'filters': ['EmailLimiter', 'require_debug_false'], }, }, 'loggers': { '': { - 'handlers': ['console', 'file', 'mail_admins'], + 'handlers': ['inapp', 'console', 'file', 'mail_admins'], 'level': 'INFO' } } diff --git a/zephyr/handlers.py b/zephyr/handlers.py new file mode 100644 index 0000000000..0258be6e41 --- /dev/null +++ b/zephyr/handlers.py @@ -0,0 +1,66 @@ +import sys +import logging +import traceback + +from django.utils.timezone import now +from django.views.debug import get_exception_reporter_filter + + +class AdminHumbugHandler(logging.Handler): + """An exception log handler that Humbugs log entries to the Humbug realm. + + If the request is passed as the first argument to the log record, + request data will be provided in the email report. + """ + + # adapted in part from django/utils/log.py + + def __init__(self): + logging.Handler.__init__(self) + + def emit(self, record): + # We have to defer imports to avoid circular imports in settings.py. + from zephyr.models import Message, UserProfile, Recipient, \ + create_stream_if_needed, get_client, do_send_message + from django.conf import settings + message = Message() + message.sender = UserProfile.objects.get(user__email="humbug+errors@humbughq.com") + message.recipient = Recipient.objects.get(type_id=create_stream_if_needed( + message.sender.realm, "devel").id, type=Recipient.STREAM) + message.pub_date = now() + message.sending_client = get_client("Internal") + + try: + request = record.request + subject = '%s (%s IP): %s' % ( + record.levelname, + (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS + and 'internal' or 'EXTERNAL'), + record.getMessage() + ) + filter = get_exception_reporter_filter(request) + request_repr = filter.get_request_repr(request) + except Exception: + subject = '%s: %s' % ( + record.levelname, + record.getMessage() + ) + request = None + request_repr = "Request repr() unavailable." + message.subject = self.format_subject(subject) + + if record.exc_info: + stack_trace = '\n'.join(traceback.format_exception(*record.exc_info)) + else: + stack_trace = 'No stack trace available' + + message.content = "~~~~ pytb\n%s\n\n%s\n~~~~" % (stack_trace, request_repr) + do_send_message(message) + + def format_subject(self, subject): + """ + Escape CR and LF characters, and limit length to 60 characters. + """ + formatted_subject = subject.replace('\n', '\\n').replace('\r', '\\r') + return formatted_subject[:60] +