A deferred logging file handler for Django

At Tangent we handle environment-specific configuration of Django projects using the method outlined by David Cramer. This involves distinguishing between core settings (which we keep in core/default.py) and environment specific settings (eg core/stage.py, core/test.py). The standard settings.py module imports all defaults and then uses a environmental shell variable to determine which environment settings module to import.

A problem

One tricky issue with this arrangement is logging to file. Ideally, we want to define a single LOGGING dict in the default settings but have file logging use an environment-specific folder. For example, logging to file in the test environment goes to /var/log/project/test/ while stage goes to a file in /var/log/project/stage.

One solution

This can be solved by using a string template for the filename argument to each FileHandler in the LOGGING setting:

# conf/default.py

LOGGING = {
    'version': 1,
    ...
    'handlers': {
        'error_file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': '{log_root}errors.log',
            'formatter': 'verbose'
        },
    },
    'loggers': {
        'django.request': {
            'handlers': ['error_file'],
            'level': 'ERROR',
            'propagate': False,
        },
    }
}

then importing the default LOGGING dict into your environment-specific settings and formatting each filename with the correct path:

# conf/test.py

from conf.default import LOGGING

LOG_ROOT = '/var/log/project/test/'
for handler in LOGGING['handlers'].values():
    if handler['class'] == 'logging.FileHandler':
        handler['filename'] = handler['filename'].format(log_root=LOG_ROOT)

This works but is rather clunky. For instance, the default LOGGING setting (without an environmental override) will lead to an error .

Another solution

Another, possibly more elegant, solution is to use a specialisd logging handler that defers evaluation of the filepath until it tries to log a record.

from logging import FileHandler as BaseFileHandler
import os


class DeferredFileHandler(BaseFileHandler):

    def __init__(self, filename, *args, **kwargs):
        self.filename = filename
        kwargs['delay'] = True
        BaseFileHandler.__init__(self, "/dev/null", *args, **kwargs)

    def _open(self):
        # We import settings here to avoid a circular reference as this module
        # will be imported when settings.py is executed.
        from django.conf import settings
        self.baseFilename = os.path.join(settings.LOG_ROOT, self.filename)
        return BaseFileHandler._open(self)

Now, all we need to do is use the new handler in our LOGGING dict:

# conf/default.py

LOGGING = {
    'version': 1,
    ...
    'handlers': {
        'error_file': {
            'level': 'INFO',
            'class': 'deferred_filelogger.DeferredFilehandler',
            'filename': 'errors.log',
            'formatter': 'verbose'
        },
    },
    'loggers': {
        'django.request': {
            'handlers': ['error_file'],
            'level': 'ERROR',
            'propagate': False,
        },
    }
}

and specify a LOG_ROOT setting for each environment:

# conf/test.py

LOG_ROOT = '/var/log/project/test/'

Such a logger is part of django-oscar, but I’ve packaged it up separately so it can be used in non-Oscar projects. The package is called django-deferred-filelogger and can be installed from PyPI using:

pip install django-deferred-filelogger
——————

Something wrong? Suggest an improvement or add a comment (see article history)
Tagged with: django, python
Filed in: tips

Previous: Conditional logic in Django forms
Next: purl, URI templates and generated tests

Copyright © 2005-2023 David Winterbottom
Content licensed under CC BY-NC-SA 4.0.