Creating custom log handler that logs to database models in django

By | September 19, 2012

I was told to create some kind of logging mechanism to insert logs into database, and make sure it’s generic. So after some thought I decided to mimic the builtin RotatingFileHandler and write a DBHandler.

The RotatingFileHandler allows you to specify which file to write to and rotate files, therefore my DBHandler should also allow you to specify which model to insert to and specify expiry in settings, and best of all, on a standalone app.

My implementation uses a separate database for logging, which needs have a router like this:

class LogRouter(object):
    def db_for_read(self, model, **hints):
        if model._meta.app_label == 'logger':
            return 'logger'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label == 'logger':
            return 'logger'
        return None

    def allow_syncdb(self, db, model):
        if db == 'logger':
            return model._meta.app_label == 'logger'
        elif model._meta.app_label == 'logger':
            return False
        return None

of course you need to tell settings you have 2 databases like this:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': '',                      # Or path to database file if using sqlite3.
        'USER': '',                      # Not used with sqlite3.
        'PASSWORD': '',                  # Not used with sqlite3.
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
    },
    'logger': {
        'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': '',                      # Or path to database file if using sqlite3.
        'USER': '',                      # Not used with sqlite3.
        'PASSWORD': '',                  # Not used with sqlite3.
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
    }
}

and we need to create some models:

from django.db import models

class DBLogEntry(models.Model):
    time = models.DateTimeField(auto_now_add=True)
    level = models.CharField(max_length=10)
    message = models.TextField()

    class Meta:
        abstract = True

class GeneralLog(DBLogEntry):
    pass

class SpeicalLog(DBLogEntry):
    pass

then we need to create a custom handler, call logging.handlers.DBHandler

rom logging import Handler
from django.utils import timezone
import json, datetime, random

class DBHandler(Handler,object):
    """
    This handler will add logs to a database model defined in settings.py
    If log message (pre-format) is a json string, it will try to apply the array onto the log event object
    """

    model_name = None
    expiry = None

    def __init__(self, model="", expiry=0):
        super(DBHandler,self).__init__()
        self.model_name = model
        self.expiry = int(expiry)

    def emit(self,record):
        # big try block here to exit silently if exception occurred
        try:
            # instantiate the model
            try:
                model = self.get_model(self.model_name)
            except:
                from logger.models import GeneralLog as model

            log_entry = model(level=record.levelname, message=self.format(record))

            # test if msg is json and apply to log record object
            try:
                data = json.loads(record.msg)
                for key,value in data.items():
                    if hasattr(log_entry,key):
                        try:
                            setattr(log_entry,key,value)
                        except:
                            pass
            except:
                pass

            log_entry.save()

            # in 20% of time, check and delete expired logs
            if self.expiry and random.randint(1,5) == 1:
                model.objects.filter(time__lt = timezone.now() - datetime.timedelta(seconds=self.expiry)).delete()
        except:
            pass

    def get_model(self, name):
        names = name.split('.')
        mod = __import__('.'.join(names[:-1]), fromlist=names[-1:])
        return getattr(mod, names[-1])

and go back to settings to specify the custom handler to log stuff like this

'handlers':{
    'log_db':{
            'level': 'INFO',
            'class': 'logger.handlers.DBHandler',
            'model': 'logger.models.SpecialLog',
            'expiry': 86400,
            'formatter': 'verbose',
            },
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.