Source code for lmi.scripts.common.lmi_logging

# Copyright (C) 2013-2014 Michal Minar <miminar@redhat.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
"""
Utilities for logging framework.
"""
import logging
import sys

from lmi.scripts.common import configuration
from lmi.scripts.common import errors

TERM_COLOR_NAMES = (
        'black',
        'red',
        'green',
        'yellow',
        'blue',
        'magenta',
        'cyan',
        'white',
        )

# Following are terminal color codes of normal mode.
CN_BLACK   = 0
CN_RED     = 1
CN_GREEN   = 2
CN_YELLOW  = 3
CN_BLUE    = 4
CN_MAGENTA = 5
CN_CYAN    = 6
CN_WHITE   = 7
# Following are terminal color codes of bright mode.
CB_BLACK   = 8
CB_RED     = 9
CB_GREEN   = 10
CB_YELLOW  = 11
CB_BLUE    = 12
CB_MAGENTA = 13
CB_CYAN    = 14
CB_WHITE   = 15

WARNING_COLOR  = CB_YELLOW
ERROR_COLOR    = CB_RED
CRITICAL_COLOR = CB_MAGENTA

#: Dictionary assigning color code to logging level.
LOG_LEVEL_2_COLOR = {
        logging.WARNING : WARNING_COLOR,
        logging.ERROR   : ERROR_COLOR,
        logging.CRITICAL: CRITICAL_COLOR
}

[docs]class LogRecord(logging.LogRecord): """ Overrides :py:class:`logging.LogRecord`. It adds new attributes: * `levelname_` - Name of level in lowercase. * `cseq` - Escape sequence for terminal used to set color assigned to particular log level. * `creset` - Escape sequence for terminal used to reset foreground color. These can be used in format strings initializing logging formatters. Accepts the same arguments as base class. """ def __init__(self, name, level, *args, **kwargs): use_colors = kwargs.pop('use_colors', True) logging.LogRecord.__init__(self, name, level, *args, **kwargs) self.levelname_ = self.levelname.lower() if use_colors and level >= logging.WARNING: if level >= logging.CRITICAL: color = LOG_LEVEL_2_COLOR[logging.CRITICAL] elif level >= logging.ERROR: color = LOG_LEVEL_2_COLOR[logging.ERROR] else: color = LOG_LEVEL_2_COLOR[logging.WARNING] self.cseq = get_color_sequence(color) self.creset = "\x1b[39m" else: self.cseq = self.creset = ''
class ScriptsLogger(logging.getLoggerClass()): #: Boolean saying whether the color sequences for log messages shall be #: generated. USE_COLORS = True def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None): """ Overriden method that just changes the *LogRecord* class to our predefined and ensures that exception's traceback is printed once at most. """ if exc_info: err = exc_info[1] if getattr(err, '_traceback_logged', False): # do not print traceback second time exc_info = None elif self.isEnabledFor(level): try: err._traceback_logged = True except AttributeError: pass rv = LogRecord(name, level, fn, lno, msg, args, exc_info, func, use_colors=self.USE_COLORS) if extra is not None: for key in extra: if (key in ["message", "asctime"]) or (key in rv.__dict__): raise KeyError("Attempt to overwrite %r in LogRecord" % key) rv.__dict__[key] = extra[key] return rv def exception(self, msg, *args, **kwargs): lmi_config = configuration.Configuration.get_instance() exc_info = sys.exc_info() err = exc_info[1] if ( lmi_config.trace and ( not isinstance(err, errors.LmiError) or ( not getattr(err, '_traceback_logged', False) and lmi_config.verbosity >= lmi_config.OUTPUT_DEBUG))): kwargs['exc_info'] = exc_info else: kwargs.pop('exc_info', None) self.error(msg, *args, **kwargs)
[docs]class LevelDispatchingFormatter(object): """ Formatter class for logging module. It allows to predefine different format string used for some level ranges. :param dict formatters: Mapping of module names to *format*. It is a dictionary of following format: :: { max_level1 : format1 , max_level2 : format2 , ... } *format* in parameters description can be either ``string`` or another formatter object. For example if you want to have format3 used for *ERROR* and *CRITICAL* levels, *format2* for *INFO* and *format1* for anything else, your dictionary will look like this: :: { logging.INFO - 1 : format1 , logging.INFO : format2 } And the *default* value should have *format3* assigned. :param default: Default *format* to use. This format is used for all levels higher than the maximum of *formatters*' keys. """ def __init__(self, formatters, default=configuration.DEFAULT_FORMAT_STRING, datefmt=None): for k, formatter in formatters.items(): if isinstance(formatter, basestring): formatters[k] = logging.Formatter(formatter, datefmt=datefmt) #: This a a tuple of pairs sorted by the first item in descending #: order (highest priority ordered first). self._formatters = tuple(sorted(formatters.items(), key=lambda t: -t[0])) if isinstance(default, basestring): default = logging.Formatter(default, datefmt=datefmt) self._default_formatter = default
[docs] def format(self, record): """ Interface for logging module. """ record.levelname_ = record.levelname.lower() formatter = self._default_formatter for level, fmt in self._formatters: if level < record.levelno: break formatter = fmt try: return formatter.format(record) except KeyError: # in some modules or libraries it may happen that logger is # initialized before our logger class is set as default record.cseq = record.creset = '' return formatter.format(record)
[docs]def get_logger(module_name): """ Convenience function for getting callable returning logger for particular module name. It's supposed to be used at module's level to assign its result to global variable like this: :: from lmi.scripts import common LOG = common.get_logger(__name__) This can be used in module's functions and classes like this: :: def module_function(param): LOG().debug("This is debug statement logging param: %s", param) Thanks to ``LOG`` being a callable, it always returns valid logger object with current configuration, which may change overtime. :param string module_name: Absolute dotted module path. :rtype: :py:class:`logging.Logger` """ def _logger(): """ Callable used to obtain current logger object. """ return logging.getLogger(module_name) return _logger
[docs]def get_color_sequence(color_code): """ Computer color sequence for particular color code. :returns: Escape sequence for terminal used to set foreground color. :rtype: str """ if color_code <= 7: return "\x1b[%dm" % (30 + color_code) return "\x1b[38;5;%dm" % color_code
[docs]def setup_logger(use_colors=True): """ This needs to be called before any logging takes place. """ ScriptsLogger.USE_COLORS = use_colors logging.setLoggerClass(ScriptsLogger)