gamunu/cassandra_snap

View on GitHub
cassandra_snap/logging_helper.py

Summary

Maintainability
A
1 hr
Test Coverage
import datetime
import errno
import logging
import os

from logging import handlers
from os import path


# Global logging handlers created by configure.
HANDLERS = []


class IndentFormatter(logging.Formatter):
    def format(self, record, *args, **kwargs):
        """
        Format a message in the log
        Act like the normal format, but indent anything that is a
        newline within the message.
        """
        return logging.Formatter.format(
            self, record, *args, **kwargs).replace('\n', '\n' + ' ' * 8)


def configure(*args, **kwargs):
    """
    Configure logging.
    Borrowed from logging.basicConfig
    Uses the IndentFormatter instead of the regular Formatter
    Also, opts the caller into Syslog output, unless syslog could not
    be opened for some reason or another, in which case a warning will
    be printed to the other log handlers.
    """
    # Configuration must only happen once: no mechanism for avoiding
    # duplication of handlers exists.
    assert len(HANDLERS) == 0

    log_destinations = get_log_destinations()

    if 'stderr' in log_destinations:
        # Add stderr output.
        HANDLERS.append(logging.StreamHandler())

    def terrible_log_output(s):
        import sys

        print >> sys.stderr, s

    places = [
        # Linux
        '/dev/log',

        # FreeBSD
        '/var/run/log',

        # Macintosh
        '/var/run/syslog',
    ]

    default_syslog_address = places[0]
    for p in places:
        if path.exists(p):
            default_syslog_address = p
            break

    syslog_address = kwargs.setdefault('syslog_address',
                                       default_syslog_address)

    valid_facility = False
    if 'syslog' in log_destinations:
        facility, valid_facility = get_syslog_facility()

        if not valid_facility:
            terrible_log_output('invalid syslog facility level specified')

        try:
            # Add syslog output.
            HANDLERS.append(handlers.SysLogHandler(syslog_address,
                                                   facility=facility))
        except EnvironmentError as e:
            if e.errno in [errno.EACCES, errno.ECONNREFUSED]:
                message = ('cassandra-snapshotter: Could not set up syslog, '
                           'continuing anyway.  '
                           'Reason: {0}').format(errno.errorcode[e.errno])

                terrible_log_output(message)

    fs = kwargs.get("format", logging.BASIC_FORMAT)
    dfs = kwargs.get("datefmt", None)
    fmt = IndentFormatter(fs, dfs)

    for handler in HANDLERS:
        handler.setFormatter(fmt)
        logging.root.addHandler(handler)

    # Default to INFO level logging.
    set_level(kwargs.get('level', logging.INFO))


def get_log_destinations():
    """Parse env string"""
    # if env var is not set default to stderr + syslog
    env = os.getenv('CASSANDRA_SNAP_LOG_DESTINATION', 'stderr,syslog')
    return env.split(",")


def get_syslog_facility():
    """Get syslog facility from ENV var"""
    facil = os.getenv('CASSANDRA_SNAP_SYSLOG_FACILITY', 'user')

    valid_facility = True
    try:
        facility = handlers.SysLogHandler.facility_names[facil.lower()]
    except KeyError:
        valid_facility = False
        facility = handlers.SysLogHandler.LOG_USER

    return facility, valid_facility


def set_level(level):
    """Adjust the logging level of CASSANDRA_SNAP"""
    for handler in HANDLERS:
        handler.setLevel(level)

    logging.root.setLevel(level)


class CassandraSnapshotterLogger(object):
    def __init__(self, *args, **kwargs):
        self._logger = logging.getLogger(*args, **kwargs)

    @staticmethod
    def _fmt_structured(d):
        """Formats '{k1:v1, k2:v2}' => 'time=... pid=... k1=v1 k2=v2'
        Output is lexically sorted, *except* the time and pid always
        come first, to assist with human scanning of the data.
        """
        timeEntry = datetime.datetime.utcnow().strftime(
            "time=%Y-%m-%dT%H:%M:%S.%f-00")
        pidEntry = "pid=" + str(os.getpid())

        rest = sorted('='.join([unicode(k), unicode(v)])
                      for (k, v) in d.items())

        return ' '.join([timeEntry, pidEntry] + rest)

    @staticmethod
    def fmt_logline(msg, detail=None, hint=None, structured=None):
        msg_parts = ['MSG: ' + msg]

        if detail is not None:
            msg_parts.append('DETAIL: ' + detail)
        if hint is not None:
            msg_parts.append('HINT: ' + hint)

        if structured is None:
            structured = {}

        msg_parts.append('STRUCTURED: ' +
                         CassandraSnapshotterLogger._fmt_structured(structured))

        return '\n'.join(msg_parts)

    def log(self, level, msg, *args, **kwargs):
        detail = kwargs.pop('detail', None)
        hint = kwargs.pop('hint', None)
        structured = kwargs.pop('structured', None)

        self._logger.log(
            level,
            self.fmt_logline(msg, detail, hint, structured),
            *args, **kwargs)

    def debug(self, *args, **kwargs):
        self.log(logging.DEBUG, *args, **kwargs)

    def info(self, *args, **kwargs):
        self.log(logging.INFO, *args, **kwargs)

    def warning(self, *args, **kwargs):
        self.log(logging.WARNING, *args, **kwargs)

    def error(self, *args, **kwargs):
        self.log(logging.ERROR, *args, **kwargs)

    def critical(self, *args, **kwargs):
        self.log(logging.CRITICAL, *args, **kwargs)