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', }, }