18F/federalist-garden-build

View on GitHub
src/log_utils/get_logger.py

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
'''
Setup nice logs.
Clients should use the `get_logger` method to get a logger instance.
'''

import sys
import logging
import logging.handlers

from .db_handler import DBHandler

DEFAULT_LOG_LEVEL = logging.INFO

LOG_ATTRS = {}


class LogFilter(logging.Filter):
    '''
    For every log message, replaces any of the values found in `priv_values`
    with the provided or default `mask` text. In addition, this prevents empty
    messages from being logged at all.
    '''
    DEFAULT_MASK = '[PRIVATE VALUE HIDDEN]'
    INVALID_ACCESS_KEY = 'InvalidAccessKeyId'

    def __init__(self, priv_vals, mask=DEFAULT_MASK):
        self.priv_vals = priv_vals
        self.mask = mask
        logging.Filter.__init__(self)

    def filter(self, record):
        for priv_val in self.priv_vals:
            record.msg = record.msg.replace(priv_val, self.mask)

        if self.INVALID_ACCESS_KEY in record.msg:
            record.msg = (
                'Whoops, our S3 keys were rotated during your '
                'build and became out of date. This was not a '
                'problem with your site build, but if you restart '
                'the failed build it should work on the next try. '
                'Sorry for the inconvenience!'
            )

        return len(record.msg) > 0


class Formatter(logging.Formatter):
    '''
    A more forgiving formatter that will fill in blank values if our custom
    attributes are missing
    '''
    def __init__(self, keys, *args, **kwargs):
        self.keys = keys
        logging.Formatter.__init__(self, *args, **kwargs)

    def format(self, record):
        '''
        Add missing values before formatting as normal
        '''
        for key in self.keys:
            if (key not in record.__dict__):
                record.__dict__[key] = ''

        return super().format(record)


def get_logger(name):
    '''
    Gets a logger instance configured with our formatter and handler
    for the given name.
    '''
    logger = logging.getLogger(name)

    return logging.LoggerAdapter(logger, LOG_ATTRS)


def set_log_attrs(attrs):
    global LOG_ATTRS
    LOG_ATTRS = attrs


def init_logging(private_values, attrs, db_url):
    global LOG_ATTRS
    LOG_ATTRS = attrs

    date_fmt = '%Y-%m-%d %H:%M:%S'
    style_fmt = '{'
    short_fmt = '{asctime} {levelname} [{name}] {message}'
    long_fmt = '{asctime} {levelname} [{name}] '
    for key in attrs.keys():
        long_fmt = long_fmt + '@' + key + ': {' + key + '} '

    long_fmt = long_fmt + '@message: {message}'

    extra_attrs = attrs.keys()

    log_filter = LogFilter(private_values)

    log_level = DEFAULT_LOG_LEVEL

    stream_formatter = Formatter(extra_attrs, long_fmt, date_fmt, style_fmt)

    stream_handler = logging.StreamHandler(sys.stdout)
    stream_handler.setFormatter(stream_formatter)
    stream_handler.setLevel(log_level)
    stream_handler.addFilter(log_filter)

    handlers = [stream_handler]

    # configure db logging
    build_id = attrs['buildid']
    db_formatter = logging.Formatter(short_fmt, date_fmt, style_fmt)

    db_handler = DBHandler(db_url, build_id)
    db_handler.setFormatter(db_formatter)
    db_handler.setLevel(log_level)
    db_handler.addFilter(log_filter)

    handlers.append(db_handler)

    logging.basicConfig(level=log_level, handlers=handlers)