Syntax highlighting for Django’s SQL query logging Syntax highlighting for Django’s SQL query logging Syntax highlighting for Django’s SQL query logging

Syntax highlighting for Django’s SQL query logging

Let’s start at the beginning: Python’s logging module is hard to understand. And it’s not much easier to configure it the way that you want it to behave. There are filters, formatters, handlers, loggers, to only name a few components.

filters
Provide a fine-grained way to suppress or allow a log record to be send to handlers.
formatters
Given a formatting string and a log record, they generate the string that is actually logged.
handlers
Define how a log record is handled. This could be an output to the console or sending an email.
loggers
Tell the origin of a message on a higher level (stack, file, line number, etc is derived automatically).

This is a simple logging example:

import logging

logger = logging.getLogger('primes.finder')
logger.debug('Next prime after %s is %s', 97, 101)

The logger effectively has the name 'primes.finder' and will be referred to by that name in Django’s LOGGING configuration. Potentially defined filters on a logger or handler would be consulted before a message is passed to a handler from the logger. Such a filter could e.g. inspect the two primes and see if the second has more digits than the first.

A handler takes care of outputting a log record. It could e.g. output the log message on a command line or send it via email. In the former case a formatter would be used to define a general format of the log messages, e.g. prepend a timestamp and log level.

In order to provide syntax highlighting for the SQL queries Django runs we will need to write our own formatter. To do that, we need to look at the way Django logs the queries (django.db.backends.utils.CursorDebugWrapper.execute()):

logger.debug(
    '(%.3f) %s; args=%s' % (duration, sql, params),
    extra={
        'duration': duration,
        'sql': sql,
        'params': params,
    }
)

What we can see here is the record object that our the formatter will receive has the attributes duration, sql and params, derived from the extra keyword argument.

class SQLFormatter(logging.Formatter):
    def format(self, record):
        # Check if Pygments is available for coloring
        try:
            import pygments
            from pygments.lexers import SqlLexer
            from pygments.formatters import TerminalTrueColorFormatter
        except ImportError:
            pygments = None

        # Check if sqlparse is available for indentation
        try:
            import sqlparse
        except ImportError:
            sqlparse = None

        # Remove leading and trailing whitespaces
        sql = record.sql.strip()

        if sqlparse:
            # Indent the SQL query
            sql = sqlparse.format(sql, reindent=True)

        if pygments:
            # Highlight the SQL query
            sql = pygments.highlight(
                sql,
                SqlLexer(),
                TerminalTrueColorFormatter(style='monokai')
            )

        # Set the record's statement to the formatted query
        record.statement = sql
        return super(SQLFormatter, self).format(record)

Update your LOGGING configuration to include the sql formatter, sql handler and django.db.backends logger:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'sql': {
            '()': 'path.to.your.SQLFormatter',
            'format': '[%(duration).3f] %(statement)s',
        }
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
        'sql': {
            'class': 'logging.StreamHandler',
            'formatter': 'sql',
            'level': 'DEBUG',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['sql'],
            'level': 'DEBUG',
            'propagate': False,
        },
        'django.db.backends.schema': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': False,
        },
    }
}

Update: In case you’re working with a 256 color terminal, you should use the Terminal256Formatter instead of the TerminalTrueColorFormatter in the SQLFormatter.format() method. (Thanks Felix Hummel).