Browse Source

Added DBLOG_DATABASE setting for multi-db

David Cramer 15 years ago
parent
commit
583bcd85e0
8 changed files with 292 additions and 53 deletions
  1. 12 0
      LICENSE
  2. 52 4
      README.rst
  3. 1 1
      djangodblog/__init__.py
  4. 121 0
      djangodblog/manager.py
  5. 4 39
      djangodblog/middleware.py
  6. 15 1
      djangodblog/models.py
  7. 79 0
      djangodblog/tests.py
  8. 8 8
      setup.py

+ 12 - 0
LICENSE

@@ -0,0 +1,12 @@
+Copyright (c) 2009 David Cramer and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+    3. Neither the name of the django-db-log nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 52 - 4
README.rst

@@ -31,17 +31,65 @@ Once installed, update your settings.py and add the middleware and installed app
 
 Finally, run ``python manage.py syncdb`` to create the database tables.
 
+Configuration
+=============
+
+Several options exist to configure django-db-log via your ``settings.py``:
+
+DBLOG_CATCH_404_ERRORS
+######################
+
+Enable catching of 404 errors in the logs. Default value is ``False``.
+
+DBLOG_DATABASE
+##############
+
+Warning: This feature is currently in the testing phase.
+
+Use a secondary database to store error logs. This is useful if you have several websites and want to aggregate error logs onto one database server::
+
+	DBLOG_DATABASE = dict(
+	    DATABASE_ENGINE='mysql', # defaults to settings.DATABASE_ENGINE
+	    DATABASE_NAME='my_db_name',
+	    DATABASE_USER='db_user',
+	    DATABASE_PASSWORD='db_pass',
+	    DATABASE_HOST='localhost', # defaults to localhost
+	    DATABASE_PORT='', # defaults to [default port]
+	    DATABASE_OPTIONS={}
+	)
+
+Some things to note:
+
+* You will need to create the tables by hand if you use this option. Use ``python manage.py sql djangodblog`` and dump that SQL into the correct server.
+* This functionality does not yet support Django 1.2.
+
 Usage
 =====
 
 You will find two new admin panels in the automatically built Django administration:
 
-* Errors
-* Error batchs
+* Errors (Error)
+* Error batches (ErrorBatch)
+
+It will store every single error inside of the `Errors` model, and it will store a collective, or summary, of errors inside of `Error batches` (this is more useful for most cases). If you are using this on multiple sites with the same database, the `Errors` table also contains the SITE_ID for which it the error appeared on.
+
+If you wish to access these within your own views and models, you may do so via the standard model API::
+
+	from djangodblog.models import Error, ErrorBatch
+	
+	ErrorBatch.objects.all().order_by('-last_seen')
+
+You can also record errors outside of middleware if you want::
 
-It will store every single error inside of the `Errors` model, and it will store a collective, or summary, of errors inside of `Error batchs` (this is more useful for most cases).
+	from djangodblog.models import Error
+	
+	try:
+		...
+	except Exception, exc:
+		Error.objects.create_from_exception(exc, [url=None])
 
 Notes
 =====
 
-* django-db-log will automatically integrate with django-idmapper
+* django-db-log will automatically integrate with django-idmapper.
+* Multi-db support (via ``DBLOG_DATABASE``) will most likely not work in Django 1.2

+ 1 - 1
djangodblog/__init__.py

@@ -1 +1 @@
-__version__ = (1, 1, 0)
+__version__ = (1, 2, 0)

+ 121 - 0
djangodblog/manager.py

@@ -0,0 +1,121 @@
+# Multi-db support based on http://www.eflorenzano.com/blog/post/easy-multi-database-support-django/
+# TODO: is there a way to use the traceback module based on an exception variable?
+
+from django.conf import settings
+from django.db import models
+from django.conf import settings
+from django.db.models import sql
+from django.db.transaction import savepoint_state
+from django.utils.hashcompat import md5_constructor
+from django.utils.encoding import smart_unicode
+from django.db.models.sql import BaseQuery
+from django.db.models.query import QuerySet
+
+try:
+    import thread
+except ImportError:
+    import dummy_thread as thread
+import traceback
+import socket
+import warnings
+import datetime
+import django
+
+django_is_10 = django.VERSION < (1, 1)
+
+"""
+``DBLOG_DATABASE`` allows you to use a secondary database for error logging::
+
+    DBLOG_DATABASE = dict(
+        DATABASE_ENGINE='mysql', # defaults to settings.DATABASE_ENGINE
+        DATABASE_NAME='my_db_name',
+        DATABASE_USER='db_user',
+        DATABASE_PASSWORD='db_pass',
+        DATABASE_HOST='localhost', # defaults to localhost
+        DATABASE_PORT='', # defaults to [default port]
+        DATABASE_OPTIONS={}
+    )
+    
+Note: You will need to create the tables by hand if you use this option.
+"""
+
+assert(not getattr(settings, 'DBLOG_DATABASE', None) or django.VERSION < (1, 2), 'The `DBLOG_DATABASE` setting requires Django < 1.2')
+
+class DBLogManager(models.Manager):
+    def get_query_set(self):
+        db_options = getattr(settings, 'DBLOG_DATABASE', None)
+        if not db_options:
+            return super(DBLogManager, self).get_query_set()
+            
+        connection = self.get_db_wrapper(db_options)
+        if connection.features.uses_custom_query_class:
+            Query = connection.ops.query_class(BaseQuery)
+        else:
+            Query = BaseQuery
+        return QuerySet(self.model, Query(self.model, connection))
+
+    def get_db_wrapper(self, options):
+        backend = __import__('django.db.backends.' + options.get('DATABASE_ENGINE', settings.DATABASE_ENGINE)
+            + ".base", {}, {}, ['base'])
+        if django_is_10:
+            backup = {}
+            for key, value in options.iteritems():
+                backup[key] = getattr(settings, key)
+                setattr(settings, key, value)
+        connection = backend.DatabaseWrapper(options)
+        # if django_is_10:
+        #     connection._cursor(settings)
+        # else:
+        #     wrapper._cursor()
+        if django_is_10:
+            for key, value in backup.iteritems():
+                setattr(settings, key, value)
+        return connection
+
+    def _insert(self, values, return_id=False, raw_values=False):
+        db_options = getattr(settings, 'DBLOG_DATABASE', None)
+        if not db_options:
+            return super(DBLogManager, self)._insert(values, return_id, raw_values)
+
+        query = sql.InsertQuery(self.model, self.get_db_wrapper())
+        query.insert_values(values, raw_values)
+        ret = query.execute_sql(return_id)
+        # XXX: Why is the following needed?
+        query.connection._commit()
+        thread_ident = thread.get_ident()
+        if thread_ident in savepoint_state:
+            del savepoint_state[thread_ident]
+        return ret
+
+    def create_from_exception(self, exception, url=None):
+        from models import Error, ErrorBatch
+        
+        server_name = socket.gethostname()
+        tb_text     = traceback.format_exc()
+        class_name  = exception.__class__.__name__
+        checksum    = md5_constructor(tb_text).hexdigest()
+
+        defaults = dict(
+            class_name  = class_name,
+            message     = smart_unicode(exception),
+            url         = url,
+            server_name = server_name,
+            traceback   = tb_text,
+        )
+
+        try:
+            instance = Error.objects.create(**defaults)
+            batch, created = ErrorBatch.objects.get_or_create(
+                class_name = class_name,
+                server_name = server_name,
+                checksum = checksum,
+                defaults = defaults
+            )
+            if not created:
+                batch.times_seen += 1
+                batch.resolved = False
+                batch.last_seen = datetime.datetime.now()
+                batch.save()
+        except Exception, exc:
+            warnings.warn(smart_unicode(exc))
+        return instance

+ 4 - 39
djangodblog/middleware.py

@@ -1,48 +1,13 @@
-import traceback
-import socket
-import warnings
-import datetime
-
 from django.conf import settings
 from django.http import Http404
-from django.utils.hashcompat import md5_constructor
-from django.utils.encoding import smart_unicode
-
-from djangodblog.models import Error, ErrorBatch
 
-__all__ = ('DBLogMiddleware', 'DBLOG_CATCH_404_ERRORS')
+from djangodblog.models import Error
 
-DBLOG_CATCH_404_ERRORS = getattr(settings, 'DBLOG_CATCH_404_ERRORS', False)
+__all__ = ('DBLogMiddleware',)
 
 class DBLogMiddleware(object):
     def process_exception(self, request, exception):
-        if not DBLOG_CATCH_404_ERRORS and isinstance(exception, Http404):
+        if not getattr(settings, 'DBLOG_CATCH_404_ERRORS', False) and isinstance(exception, Http404):
             return
-        server_name = socket.gethostname()
-        tb_text     = traceback.format_exc()
-        class_name  = exception.__class__.__name__
-        checksum    = md5_constructor(tb_text).hexdigest()
-
-        defaults = dict(
-            class_name  = class_name,
-            message     = smart_unicode(exception),
-            url         = request.build_absolute_uri(),
-            server_name = server_name,
-            traceback   = tb_text,
-        )
 
-        try:
-            Error.objects.create(**defaults)
-            batch, created = ErrorBatch.objects.get_or_create(
-                class_name = class_name,
-                server_name = server_name,
-                checksum = checksum,
-                defaults = defaults
-            )
-            if not created:
-                batch.times_seen += 1
-                batch.resolved = False
-                batch.last_seen = datetime.datetime.now()
-                batch.save()
-        except Exception, exc:
-            warnings.warn(smart_unicode(exc))
+        Error.objects.create_from_exception(exception, url=request.build_absolute_uri())

+ 15 - 1
djangodblog/models.py

@@ -1,5 +1,7 @@
 from django.db import models
+from django.conf import settings
 from django.utils.translation import ugettext_lazy as _
+
 try:
     from idmapper.models import SharedMemoryModel as Model
 except ImportError:
@@ -7,6 +9,8 @@ except ImportError:
 
 import datetime
 
+from manager import DBLogManager
+
 __all__ = ('Error', 'ErrorBatch')
 
 class ErrorBatch(Model):
@@ -21,9 +25,14 @@ class ErrorBatch(Model):
     server_name     = models.CharField(max_length=128, db_index=True)
     checksum        = models.CharField(max_length=32, db_index=True)
 
+    objects         = DBLogManager()
+
     class Meta:
         unique_together = (('class_name', 'server_name', 'checksum'),)
         verbose_name_plural = 'Error batches'
+    
+    def __unicode__(self):
+        return "(%s) %s: %s" % (self.times_seen, self.class_name, self.message)
 
 class Error(Model):
     class_name      = models.CharField(_('type'), max_length=128)
@@ -31,4 +40,9 @@ class Error(Model):
     traceback       = models.TextField()
     datetime        = models.DateTimeField(default=datetime.datetime.now)
     url             = models.URLField(verify_exists=False, null=True, blank=True)
-    server_name     = models.CharField(max_length=128, db_index=True)
+    server_name     = models.CharField(max_length=128, db_index=True)
+
+    objects         = DBLogManager()
+
+    def __unicode__(self):
+        return "%s: %s" % (self.class_name, self.message)

+ 79 - 0
djangodblog/tests.py

@@ -0,0 +1,79 @@
+from django.test.client import Client
+from django.test import TestCase
+from django.core.handlers.wsgi import WSGIRequest
+from django.conf import settings
+
+from models import Error, ErrorBatch
+from middleware import DBLogMiddleware
+
+class RequestFactory(Client):
+    # Used to generate request objects.
+    def request(self, **request):
+        environ = {
+            'HTTP_COOKIE': self.cookies,
+            'PATH_INFO': '/',
+            'QUERY_STRING': '',
+            'REQUEST_METHOD': 'GET',
+            'SCRIPT_NAME': '',
+            'SERVER_NAME': 'testserver',
+            'SERVER_PORT': 80,
+            'SERVER_PROTOCOL': 'HTTP/1.1',
+        }
+        environ.update(self.defaults)
+        environ.update(request)
+        return WSGIRequest(environ)
+ 
+RF = RequestFactory()
+
+class DBLogTestCase(TestCase):
+    def testMiddleware(self):
+        request = RF.get("/", REMOTE_ADDR="127.0.0.1:8000")
+
+        ttl = (Error.objects.count(), ErrorBatch.objects.count())
+
+        try:
+            Error.objects.get(id=999999999)
+        except Error.DoesNotExist, exc:
+            DBLogMiddleware().process_exception(request, exc)
+        else:
+            self.fail('Unable to create `Error` entry.')
+        
+        cur = (Error.objects.count()-1, ErrorBatch.objects.count()-1)
+        self.assertEquals(cur, ttl, 'Counts do not match. Assumed logs failed to save. %s != %s' % (cur, ttl))
+        
+    def testAPI(self):
+        ttl = (Error.objects.count(), ErrorBatch.objects.count())
+
+        try:
+            Error.objects.get(id=999999999)
+        except Error.DoesNotExist, exc:
+            Error.objects.create_from_exception(exc)
+        else:
+            self.fail('Unable to create `Error` entry.')
+        
+        cur = (Error.objects.count()-1, ErrorBatch.objects.count()-1)
+        self.assertEquals(cur, ttl, 'Counts do not match. Assumed logs failed to save. %s != %s' % (cur, ttl))
+        
+    def testAlternateDatabase(self):
+        settings.DBLOG_DATABASE = dict(
+            DATABASE_HOST=settings.DATABASE_HOST,
+            DATABASE_PORT=settings.DATABASE_PORT,
+            DATABASE_NAME=settings.DATABASE_NAME,
+            DATABASE_USER=settings.DATABASE_USER,
+            DATABASE_PASSWORD=settings.DATABASE_PASSWORD,
+            DATABASE_OPTIONS=settings.DATABASE_OPTIONS,
+        )
+        
+        ttl = (Error.objects.count(), ErrorBatch.objects.count())
+
+        try:
+            Error.objects.get(id=999999999)
+        except Error.DoesNotExist, exc:
+            Error.objects.create_from_exception(exc)
+        else:
+            self.fail('Unable to create `Error` entry.')
+            
+        cur = (Error.objects.count()-1, ErrorBatch.objects.count()-1)
+        self.assertEquals(cur, ttl, 'Counts do not match. Assumed logs failed to save. %s != %s' % (cur, ttl))
+
+        settings.DBLOG_DATABASE = None        

+ 8 - 8
setup.py

@@ -6,9 +6,9 @@ import djangodblog
 
 setup(
     name='django-db-log',
-    version=".".join(map(str, djangodblog.__version__)),
-    author="David Cramer",
-    author_email="dcramer@gmail.com",
+    version='.'.join(map(str, djangodblog.__version__)),
+    author='David Cramer',
+    author_email='dcramer@gmail.com',
     url='http://github.com/dcramer/django-db-log',
     install_requires=[
         'Django>=1.0'
@@ -17,10 +17,10 @@ setup(
     packages=find_packages(),
     include_package_data=True,
     classifiers=[
-        "Framework :: Django",
-        "Intended Audience :: Developers",
-        "Intended Audience :: System Administrators",
-        "Operating System :: OS Independent",
-        "Topic :: Software Development"
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'Intended Audience :: System Administrators',
+        'Operating System :: OS Independent',
+        'Topic :: Software Development'
     ],
 )