You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

264 lines
7.3 KiB

#!/usr/bin/env python
# coding=utf-8
#
# Copyright © Splunk, Inc. All Rights Reserved.
from __future__ import absolute_import, division, print_function, unicode_literals
from builtins import object
from traceback import format_tb
import logging
import sys
from os.path import basename, splitext
from . internal import string
__all__ = ['SlimLogger', 'SlimExternalFormatter']
logging.STEP = logging.INFO - 1
logging.addLevelName(logging.STEP, 'STEP')
class SlimFormatter(logging.Formatter):
"""
Format CLI messages to include the level name prefix strings we want
For example, <command>: <level_name> <message + args>
"""
# noinspection PyShadowingBuiltins
def __init__(self, formatstr):
logging.Formatter.__init__(self, formatstr)
def format(self, record):
record.levelname = self._level_names.get(record.levelno, ' ')
record.msg = '%s' * len(record.args)
return logging.Formatter.format(self, record)
_level_names = {
logging.DEBUG: ' [DEBUG] ',
logging.INFO: ' [INFO] ',
logging.WARN: ' [WARNING] ',
logging.ERROR: ' [ERROR] ',
logging.FATAL: ' [FATAL] '
}
class SlimExternalFormatter(logging.Formatter):
"""
Provide a formatter for clients of the API to use, which does not use the
same formatting at the CLI output (for raw message + args output)
"""
# noinspection PyShadowingBuiltins
def __init__(self, formatstr):
logging.Formatter.__init__(self, formatstr)
def format(self, record):
record.msg = '%s' * len(record.args)
return logging.Formatter.format(self, record)
def is_string(log) :
return type(log) == string
class SlimLogger(object):
"""
All SLIM logging, configuration, and tracking is routed through the SlimLogger
"""
# region Logging and logging count methods
@classmethod
def debug(cls, *args):
cls._emit(logging.DEBUG, *args)
@classmethod
def error(cls, *args):
cls._emit(logging.ERROR, *args)
@classmethod
def error_count(cls):
return cls._message_count[logging.ERROR]
@classmethod
def fatal(cls, *args, **kwargs):
exception_info = kwargs.get('exception_info')
if exception_info is None:
cls._emit(logging.FATAL, *args)
else:
error_type, error_value, traceback = exception_info
message = string(error_type.__name__ if error_value is None else error_value)
if cls._debug:
message += '\nTraceback: ' + error_type.__name__ + '\n' + ''.join(format_tb(traceback))
cls._emit(logging.FATAL, *(args + (': ', message)) if len(args) > 0 else message)
sys.exit(1)
@classmethod
def information(cls, *args):
cls._emit(logging.INFO, *args)
@classmethod
def step(cls, *args):
cls._emit(logging.STEP, *args)
@classmethod
def warning(cls, *args):
cls._emit(logging.WARN, *args)
@classmethod
def message(cls, level, *args, **kwargs):
if str(level) == level:
level = logging.INFO if level not in cls._level_names else cls._level_names[level.upper()]
if level != logging.FATAL:
cls._emit(level, *args)
return
cls.fatal(*args, **kwargs)
@classmethod
def reset_counts(cls):
for level in cls._message_count:
cls._message_count[level] = 0
@classmethod
def reset_messages(cls):
cls._messages_ = []
@classmethod
def exit_on_error(cls):
if cls._message_count[logging.ERROR]:
#sys.exit(1)
return True
# endregion
# region Logging configuration methods
@classmethod
def set_debug(cls, value):
cls._debug = bool(value)
@classmethod
def is_debug_enabled(cls):
return cls._debug is True
@classmethod
def set_level(cls, value):
cls._logger.setLevel(value) # setLevel() handles both numeric and string levels
cls._default_level = cls._logger.level # this level value is always numeric
@classmethod
def set_command_name(cls, command_name):
cls._adapter = logging.LoggerAdapter(cls._logger, {'command_name': command_name})
# endregion
# region Logging handlers
@classmethod
def add_handler(cls, handler):
cls._logger.addHandler(handler)
@classmethod
def remove_handler(cls, handler):
cls._logger.removeHandler(handler)
@classmethod
def handlers(cls):
return cls._logger.handlers
@classmethod
def use_external_handler(cls, handler):
cls.remove_handler(cls._handler)
cls.add_handler(handler)
@classmethod
def add_message(cls, msg):
cls._messages_.append(msg)
@classmethod
def get_message(cls):
return(cls._messages_)
# endregion
# region Privates
_debug = False # turns on debug output which is different than turning on debug messages using, e.g., set_level
_default_level = logging.STEP # call SlimLogger.set_level to change (works in tandem with SlimLogger.set_quiet)
_message_count = {
logging.STEP: 0,
logging.DEBUG: 0,
logging.INFO: 0,
logging.WARN: 0,
logging.ERROR: 0,
logging.FATAL: 0
}
_messages_ = []
# This is a copy of logging/__init__.py:_levelNames because the logging library does not expose this mapping
# It only exposes the number => string mapping via getLevelName()
_level_names = {
'CRITICAL': logging.CRITICAL,
'ERROR': logging.ERROR,
'WARN': logging.WARNING,
'WARNING': logging.WARNING,
'INFO': logging.INFO,
'DEBUG': logging.DEBUG,
'STEP': logging.STEP, # custom logging level
'NOTE': logging.INFO, # backwards compatibility
}
_level_codes = {
logging.CRITICAL:'CRITICAL',
logging.ERROR:'ERROR',
logging.WARNING:'WARNING',
logging.INFO:'INFO',
logging.DEBUG:'DEBUG',
logging.STEP:'STEP', # custom logging level
}
# noinspection PyShadowingNames
@classmethod
def _emit(cls, level, *args):
msg = (" ").join(list(filter(is_string,args)))
if msg.startswith(": ") :
msg = msg[2:]
cls.add_message({"level": cls._level_codes[level], "msg" : msg})
cls._adapter.log(level, None, *args)
cls._message_count[level] += 1
@staticmethod
def _initialize_logging():
command_name = splitext(basename(sys.argv[0]))[0]
if not command_name:
# Make sure we have a non-empty command name, even when loaded as a library
# The command name is used to grab and configure a unique non-root logger
command_name = 'slim'
logger = logging.getLogger(command_name)
logger.setLevel(logging.STEP)
logger.propagate = False # do not try to use parent logging handlers
handler = logging.StreamHandler()
handler.setFormatter(SlimFormatter('%(command_name)s:%(levelname)s%(message)s'))
logger.addHandler(handler)
adapter = logging.LoggerAdapter(logger, {'command_name': command_name})
return logger, handler, adapter
_logger, _handler, _adapter = _initialize_logging.__func__()
# endregion
pass # pylint: disable=unnecessary-pass