refactor logging and switch to python standard library

This commit is contained in:
Robert Kaussow 2019-03-28 16:54:45 +01:00
parent f92c77f5b8
commit 0ffdf36a0d
6 changed files with 209 additions and 113 deletions

View File

@ -16,11 +16,11 @@ from distutils.version import LooseVersion
import ansible import ansible
from appdirs import AppDirs from appdirs import AppDirs
from ansiblelater.utils import (abort, error, get_property, info, from . import logger
from .settings import Settings
from ansiblelater.utils import (get_property,
is_line_in_ranges, lines_ranges, is_line_in_ranges, lines_ranges,
read_standards, standards_latest, warn) read_standards, standards_latest)
try: try:
# Ansible 2.4 import of module loader # Ansible 2.4 import of module loader
@ -31,13 +31,8 @@ except ImportError:
except ImportError: except ImportError:
from ansible.utils import module_finder as module_loader from ansible.utils import module_finder as module_loader
try: settings = Settings()
import ConfigParser as configparser logger = logger.get_logger(__name__, settings.config["logging"]["level"])
except ImportError:
import configparser
class Standard(object): class Standard(object):
@ -281,16 +276,16 @@ def candidate_review(candidate, settings, lines=None):
candidate.version = standards_latest(standards.standards) candidate.version = standards_latest(standards.standards)
if candidate.expected_version: if candidate.expected_version:
if isinstance(candidate, RoleFile): if isinstance(candidate, RoleFile):
warn("%s %s is in a role that contains a meta/main.yml without a declared " logger.warn("%s %s is in a role that contains a meta/main.yml without a declared "
"standards version. " "standards version. "
"Using latest standards version %s" % "Using latest standards version %s" %
(type(candidate).__name__, candidate.path, candidate.version), (type(candidate).__name__, candidate.path, candidate.version),
settings) settings)
else: else:
warn("%s %s does not present standards version. " logger.warn("%s %s does not present standards version. "
"Using latest standards version %s" % "Using latest standards version %s" %
(type(candidate).__name__, candidate.path, candidate.version), (type(candidate).__name__, candidate.path, candidate.version),
settings) settings)
info("%s %s declares standards version %s" % info("%s %s declares standards version %s" %
(type(candidate).__name__, candidate.path, candidate.version), (type(candidate).__name__, candidate.path, candidate.version),

View File

@ -1,56 +1,45 @@
#!/usr/bin/env python #!/usr/bin/env python
import argparse
import json
import logging import logging
import optparse
import os import os
import sys import sys
from appdirs import AppDirs from ansiblelater import __version__, settings, logger
from pkg_resources import resource_filename from ansiblelater.utils import get_property
from ansiblelater import classify, settings # from .settings import Settings
from ansiblelater.utils import get_property, info, warn
from .settings import Settings
def main(): def main():
config_dir = AppDirs("ansible-later").user_config_dir parser = argparse.ArgumentParser(
default_config_file = os.path.join(config_dir, "config.yml") description="Validate ansible files against best pratice guideline")
parser.add_argument('-c', dest='config_file',
parser = optparse.OptionParser("%prog playbook_file|role_file|inventory_file", help="Location of configuration file: [%s]" % settings.config_file)
version="%prog " + get_property("__version__")) parser.add_argument('-d', dest='rules.standards',
parser.add_option('-c', dest='config_file', default=default_config_file, help="Location of standards rules")
help="Location of configuration file: [%s]" % default_config_file) parser.add_argument('-q', dest='logging.level', action="store_const",
parser.add_option('-d', dest='rules_dir', const=logging.ERROR, help="Only output errors")
help="Location of standards rules") parser.add_argument('-s', dest='rules.filter', action='append',
parser.add_option('-q', dest='log_level', action="store_const", help="limit standards to specific names")
const=logging.ERROR, help="Only output errors") parser.add_argument('-v', '--verbose', dest='logging.level', action="count",
parser.add_option('-s', dest='standards_filter', action='append',
help="limit standards to specific names")
parser.add_option('-v', '--verbose', dest='log_level', action="count",
help="Show more verbose output") help="Show more verbose output")
parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__))
options, args = parser.parse_args(sys.argv[1:]) args = parser.parse_args().__dict__
settings = Settings(options) # Override correct log level from argparse
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
if args.get("logging.level"):
args["logging.level"] = levels[min(len(levels) - 1, args["logging.level"] - 1)]
# print(settings.rulesdir) settings.set_args(args)
# settings = read_config(options.configfile)
# print(json.dumps(settings.config, indent=4, sort_keys=True))
# print(settings.config["logging"]["level"])
# # Merge CLI options with config options. CLI options override config options.
# for key, value in options.__dict__.items():
# if value:
# setattr(settings, key, value)
# if os.path.exists(settings.configfile):
# info("Using configuration file: %s" % settings.configfile, settings)
# else:
# warn("No configuration file found at %s" % settings.configfile, settings, file=sys.stderr)
# if not settings.rulesdir:
# rules_dir = os.path.join(resource_filename('ansiblelater', 'examples'))
# warn("Using example standards found at %s" % rules_dir, settings, file=sys.stderr)
# settings.rulesdir = rules_dir
# if len(args) == 0: # if len(args) == 0:
# candidates = [] # candidates = []

111
ansiblelater/logger.py Normal file
View File

@ -0,0 +1,111 @@
import logging
import os
import sys
import colorama
from pythonjsonlogger import jsonlogger
from ansible.module_utils.parsing.convert_bool import boolean as to_bool
def should_do_markup():
py_colors = os.environ.get('PY_COLORS', None)
if py_colors is not None:
return to_bool(py_colors, strict=False)
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
colorama.init(autoreset=True, strip=not should_do_markup())
class LogFilter(object):
"""
A custom log filter which excludes log messages above the logged
level.
"""
def __init__(self, level):
self.__level = level
def filter(self, logRecord): # pragma: no cover
# https://docs.python.org/3/library/logging.html#logrecord-attributes
return logRecord.levelno <= self.__level
def get_logger(name=None, level=logging.DEBUG, json=False):
"""
Build a logger with the given name and returns the logger.
:param name: The name for the logger. This is usually the module
name, ``__name__``.
:return: logger object
"""
logger = logging.getLogger(name)
logger.setLevel(level)
#handler = logging.StreamHandler()
#formatter = jsonlogger.JsonFormatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
#formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
#handler.setFormatter(formatter)
logger.addHandler(_get_error_handler(json=json))
logger.addHandler(_get_warn_handler(json=json))
logger.addHandler(_get_info_handler(json=json))
logger.propagate = False
return logger
def _get_error_handler(json=False):
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.ERROR)
handler.addFilter(LogFilter(logging.ERROR))
handler.setFormatter(logging.Formatter(error('%(message)s')))
if json:
handler.setFormatter(jsonlogger.JsonFormatter('%(message)s'))
return handler
def _get_warn_handler(json=False):
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.WARN)
handler.addFilter(LogFilter(logging.WARN))
handler.setFormatter(logging.Formatter(warn('%(message)s')))
if json:
handler.setFormatter(jsonlogger.JsonFormatter('%(message)s'))
return handler
def _get_info_handler(json=False):
handler = logging.StreamHandler(sys.stderr)
handler.setLevel(logging.INFO)
handler.addFilter(LogFilter(logging.INFO))
handler.setFormatter(logging.Formatter(info('%(message)s')))
if json:
handler.setFormatter(jsonlogger.JsonFormatter('%(message)s'))
return handler
def abort(message, file=sys.stderr):
return color_text(colorama.Fore.RED, "FATAL: {}".format(message))
sys.exit(1)
def error(message):
return color_text(colorama.Fore.RED, "ERROR: {}".format(message))
def warn(message):
return color_text(colorama.Fore.YELLOW, "WARN: {}".format(message))
def info(message):
return color_text(colorama.Fore.BLUE, "INFO: {}".format(message))
def color_text(color, msg):
return '{}{}{}'.format(color, msg, colorama.Style.RESET_ALL)

View File

@ -1,42 +1,60 @@
import json
import logging import logging
import os import os
import six
import anyconfig import anyconfig
from appdirs import AppDirs
from pkg_resources import resource_filename from pkg_resources import resource_filename
from ansiblelater import utils from ansiblelater import utils, logger
try: config_dir = AppDirs("ansible-later").user_config_dir
import ConfigParser as configparser default_config_file = os.path.join(config_dir, "config.yml")
except ImportError:
import configparser logger = logger.get_logger(__name__)
class NewInitCaller(type):
def __call__(cls, *args, **kwargs):
obj = type.__call__(cls, *args, **kwargs)
obj.after_init()
return obj
@six.add_metaclass(NewInitCaller)
class Settings(object): class Settings(object):
def __init__(self, args={}): def __init__(self, args={}, config_file=default_config_file):
self.args = self._get_args(args) self.args = args
self.config_file = config_file
self.config = self._get_config() self.config = self._get_config()
def _get_args(self, args): def set_args(self, args={}):
# Override correct log level from argparse self.config_file = args.get("config_file") or default_config_file
levels = [logging.WARNING, logging.INFO, logging.DEBUG]
if args.log_level:
args.log_level = levels[min(len(levels) - 1, args.log_level - 1)]
args_dict = dict(filter(lambda item: item[1] is not None, args.__dict__.items())) args.pop("config_file", None)
return args_dict args = dict(filter(lambda item: item[1] is not None, args.items()))
args_dict = {}
for key, value in args.items():
args_dict = utils.add_dict_branch(args_dict, key.split("."), value)
self.args = args_dict
self.config = self._get_config()
self._validate()
def _get_config(self): def _get_config(self):
defaults = self._get_defaults() defaults = self._get_defaults()
config_file = self.args.get('config_file') config_file = self.config_file
cli_options = self.args
if config_file and os.path.exists(config_file): if config_file and os.path.exists(config_file):
with utils.open_file(config_file) as stream: with utils.open_file(config_file) as stream:
s = stream.read() s = stream.read()
anyconfig.merge(defaults, utils.safe_load(s), ac_merge=anyconfig.MS_DICTS) anyconfig.merge(defaults, utils.safe_load(s), ac_merge=anyconfig.MS_DICTS)
print(json.dumps(defaults, indent=4, sort_keys=True)) if cli_options:
anyconfig.merge(defaults, cli_options, ac_merge=anyconfig.MS_DICTS)
return defaults return defaults
def _get_defaults(self): def _get_defaults(self):
@ -44,13 +62,21 @@ class Settings(object):
return { return {
'rules': { 'rules': {
'standards': self.args.get('rules_dir', rules_dir), 'standards': rules_dir,
'standards_filter': [], 'filter': [],
}, },
'logging': { 'logging': {
'level': self.args.get('log_level', logging.WARN), 'level': logging.WARN,
}, },
'ansible': { 'ansible': {
'custom_modules': [], 'custom_modules': [],
} }
} }
def after_init(self):
self.config = self._get_config()
self._validate()
def _validate(self):
logger.setLevel(self.config["logging"]["level"])

View File

@ -17,41 +17,6 @@ try:
except ImportError: except ImportError:
import configparser import configparser
def should_do_markup():
py_colors = os.environ.get('PY_COLORS', None)
if py_colors is not None:
return to_bool(py_colors, strict=False)
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
colorama.init(autoreset=True, strip=not should_do_markup())
def abort(message, file=sys.stderr):
return color_text(colorama.Fore.RED, "FATAL: {}".format(message))
sys.exit(1)
def error(message, file=sys.stderr):
return color_text(colorama.Fore.RED, "ERROR: {}".format(message))
def warn(message, settings, file=sys.stdout):
if settings.log_level <= logging.WARNING:
return color_text(colorama.Fore.YELLOW, "WARN: {}".format(message))
def info(message, settings, file=sys.stdout):
if settings.log_level <= logging.INFO:
return color_text(colorama.Fore.BLUE, "INFO: {}".format(message))
def color_text(color, msg):
print('{}{}{}'.format(color, msg, colorama.Style.RESET_ALL))
def count_spaces(c_string): def count_spaces(c_string):
leading_spaces = 0 leading_spaces = 0
trailing_spaces = 0 trailing_spaces = 0
@ -137,3 +102,13 @@ def open_file(filename, mode='r'):
""" """
with open(filename, mode) as stream: with open(filename, mode) as stream:
yield stream yield stream
def add_dict_branch(tree, vector, value):
key = vector[0]
tree[key] = value \
if len(vector) == 1 \
else add_dict_branch(tree[key] if key in tree else {},
vector[1:],
value)
return tree

View File

@ -11,7 +11,7 @@ max-line-length = 100
inline-quotes = double inline-quotes = double
exclude = .git,.tox,__pycache__,build,dist,tests,*.pyc,*.egg-info,.cache,.eggs exclude = .git,.tox,__pycache__,build,dist,tests,*.pyc,*.egg-info,.cache,.eggs
application-import-names = ansiblelater application-import-names = ansiblelater
format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s #format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
[isort] [isort]
default_section = THIRDPARTY default_section = THIRDPARTY