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