diff --git a/atomicapp/__init__.py b/atomicapp/__init__.py index 24dd79f8..e0c998d3 100644 --- a/atomicapp/__init__.py +++ b/atomicapp/__init__.py @@ -16,26 +16,25 @@ You should have received a copy of the GNU Lesser General Public License along with Atomic App. If not, see . """ - import logging - - -def set_logging(name="atomicapp", level=logging.DEBUG): - # create logger - logger = logging.getLogger() - logger.handlers = [] - logger.setLevel(level) - - # create console handler - ch = logging.StreamHandler() - - # create formatter - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - - # add formatter to ch - ch.setFormatter(formatter) - - # add ch to logger - logger.addHandler(ch) - -set_logging(level=logging.DEBUG) # override this however you want +import os +from atomicapp.constants import LOG_FILE, LOG_NAME + +# Let's make sure logging works upon first startup +path = LOG_FILE + +# Touch if it doesn't exist +if not (os.path.exists(path)): + try: + os.utime(path, None) + except: + open(path, 'a').close() + +if not (os.access(path, os.W_OK)): + raise RuntimeError("%s is not writeable" % path) + +# Setup log handling +handler = logging.FileHandler(path) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logging.getLogger(LOG_NAME).addHandler(handler) diff --git a/atomicapp/cli/main.py b/atomicapp/cli/main.py index a66ecd3b..69c563c0 100644 --- a/atomicapp/cli/main.py +++ b/atomicapp/cli/main.py @@ -210,6 +210,15 @@ def create_parser(self): default=ANSWERS_FILE_SAMPLE_FORMAT, choices=['ini', 'json', 'xml', 'yaml'], help="The format for the answers.conf.sample file. Default: %s" % ANSWERS_FILE_SAMPLE_FORMAT) + globals.parser.add_argument( + "--logging-output", + dest="logging_output", + choices=['cockpit', 'stdout', 'syslog', 'none'], + help="Override the default logging output." + "stdout: We will only log to stdout/stderr" + "cockpit: Used with cockpit integration" + "syslog: Outputs to OS default syslog daemon" + "none: atomicapp will disable any logging") # === "run" SUBPARSER === run_subparser = toplevel_subparsers.add_parser( diff --git a/atomicapp/constants.py b/atomicapp/constants.py index 2d21ec88..3aebd53b 100644 --- a/atomicapp/constants.py +++ b/atomicapp/constants.py @@ -48,6 +48,24 @@ ANSWERS_FILE_SAMPLE_FORMAT = 'ini' WORKDIR = ".workdir" LOCK_FILE = "/run/lock/atomicapp.lock" + +LOG_FILE = "/var/log/atomicapp.log" +LOG_SYSLOG = "/dev/log" +LOG_NAME = "atomicapp" +LOG_CUSTOM_NAME = "atomicapp-custom" +LOG_LEVELS = { + "notset": 0, + "debug": 10, + "info": 20, + "warning": 30, + "error": 40, + "critical": 50, + "default": 90, + "cockpit": 91, + "stdout": 92, + "syslog": 93, + "none": 94 +} HOST_DIR = "/host" DEFAULT_PROVIDER = "kubernetes" diff --git a/atomicapp/display.py b/atomicapp/display.py new file mode 100644 index 00000000..1975311d --- /dev/null +++ b/atomicapp/display.py @@ -0,0 +1,221 @@ +""" + Copyright 2015 Red Hat, Inc. + + This file is part of Atomic App. + + Atomic App is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Atomic App 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Atomic App. If not, see . +""" + +import os +import sys +import time +import logging +import logging.handlers +from atomicapp.constants import LOG_NAME, LOG_LEVELS, LOG_CUSTOM_NAME, LOG_SYSLOG + + +class Display: + + ''' + + In order to effectively use logging within Python, we manipulate the level + codes by adding our own custom ones. By doing so, we are able to have different + log legs effectively span all Python files. + + Upon initialization, Atomic App checks to see if --logging-ouput= is set during + initalization and set's the appropriate log level based on either the default + or custom level value. + + Default python level codes + + NOTSET 0 + DEBUG 10 + INFO 20 + WARNING 30 + ERROR 40 + CRITICAL 50 + + Custom log levels in constants.py + + LOG_LEVELS = { + "default": 90, + "cockpit": 91, + "stdout": 92, + "syslog": 93, + "none": 94 + } + + ''' + + # Console colour codes + codeCodes = { + 'black': '0;30', 'bright gray': '0;37', + 'blue': '0;34', 'white': '1;37', + 'green': '0;32', 'bright blue': '1;34', + 'cyan': '0;36', 'bright green': '1;32', + 'red': '0;31', 'bright cyan': '1;36', + 'purple': '0;35', 'bright red': '1;31', + 'yellow': '0;33', 'bright purple': '1;35', + 'dark gray': '1;30', 'bright yellow': '1;33', + 'normal': '0' + } + + # Upon initialization we grab the log level value and verbose level (-v or not) + def __init__(self): + self.logger = logging.getLogger(LOG_NAME) + self.verbose_level = self.logger.getEffectiveLevel() + self.log_level = logging.getLogger(LOG_CUSTOM_NAME).getEffectiveLevel() + + def debug(self, msg, only=None): + self.display(msg, 10, 'cyan', only) + + def verbose(self, msg, only=None): + self.display(msg, 10, 'cyan', only) + + def info(self, msg, only=None): + self.display(msg, 20, 'green', only) + + def warning(self, msg, only=None): + self.display(msg, 30, 'yellow', only) + + def error(self, msg, only=None): + self.display(msg, 40, 'red', only) + + # Display checks to see what log_level is being matched to and passes it along to + # the logging provider + def display(self, msg, code, color, only): + if self.log_level == LOG_LEVELS['cockpit']: + self._cockpit(msg, code, only) + elif self.log_level == LOG_LEVELS['stdout']: + self._stdout(msg, code) + elif self.log_level == LOG_LEVELS['syslog']: + self._syslog(msg, code) + elif self.log_level == LOG_LEVELS['none']: + return + else: + self._default(msg, code, color) + + # Make sure that we're outputting stdout or stderr each time + def _sysflush(self): + if self.verbose_level is LOG_LEVELS['error'] or self.verbose_level is LOG_LEVELS['warning']: + sys.stderr.flush() + else: + sys.stdout.flush() + + # Due to cockpit logging requirements, we will ONLY output logging that is designed as + # display.info("foo bar", "cockpit") + def _cockpit(self, msg, code, only): + if only is not "cockpit": + return + + if self.verbose_level is not LOG_LEVELS['debug'] and code is LOG_LEVELS['debug']: + return + + if code == LOG_LEVELS['debug']: + msg = "atomicapp.status.debug.message=%s" % msg + elif code == LOG_LEVELS['warning']: + msg = "atomicapp.status.warning.message=%s" % msg + elif code == LOG_LEVELS['error']: + msg = "atomicapp.status.error.message=%s" % msg + else: + msg = "atomicapp.status.info.message=%s" % msg + + self._sysflush() + print(self._make_unicode(msg)) + + def _syslog(self, msg, code): + if self.verbose_level is not LOG_LEVELS['debug'] and code is LOG_LEVELS['debug']: + return + + # Don't output anything, but everything into the syslog (/var/log/atomicapp.log) + handler = logging.handlers.SysLogHandler(address=LOG_SYSLOG) + self.logger.handlers = [] + self.logger.addHandler(handler) + + if code == LOG_LEVELS['debug']: + self.logger.info(msg) + elif code == LOG_LEVELS['warning']: + self.logger.warning(msg) + elif code == LOG_LEVELS['error']: + self.logger.error(msg) + else: + self.logger.info(msg) + + def _default(self, msg, code, color): + if self.verbose_level is not LOG_LEVELS['debug'] and code is LOG_LEVELS['debug']: + return + + if code == LOG_LEVELS['debug']: + self.logger.info(msg) + msg = "[DEBUG] %6d %0.2f %s" % (os.getpid(), time.time(), msg) + elif code == LOG_LEVELS['warning']: + self.logger.warning(msg) + msg = "[WARNING] %s" % msg + elif code == LOG_LEVELS['error']: + self.logger.error(msg) + msg = "[ERROR] %s" % msg + else: + self.logger.info(msg) + msg = "[INFO] %s" % msg + + self._sysflush() + print(self._colorize(self._make_unicode(msg), color)) + + def _stdout(self, msg, code): + if self.verbose_level is not LOG_LEVELS['debug'] and code is LOG_LEVELS['debug']: + return + + if code == LOG_LEVELS['debug']: + msg = "[DEBUG] %6d %0.2f %s" % (os.getpid(), time.time(), msg) + elif code == LOG_LEVELS['warning']: + msg = "[WARNING] %s" % msg + elif code == LOG_LEVELS['error']: + msg = "[ERROR] %s" % msg + else: + msg = "[INFO] %s" % msg + + self._sysflush() + print(self._make_unicode(msg)) + + # Colors! + def _colorize(self, text, color): + return "\033[" + self.codeCodes[color] + "m" + text + "\033[0m" + + # Convert all those pesky log messages to utf-8 + def _make_unicode(self, input): + if type(input) != unicode: + input = input.decode('utf-8') + return input + else: + return input + + +def set_logging(args): + if args.verbose: + level = logging.DEBUG + elif args.quiet: + level = logging.WARNING + else: + level = logging.INFO + + # Let's check to see if any of our choices match the LOG_LEVELS constant! --logging-output + for k in LOG_LEVELS: + if args.logging_output == k: + custom_level = LOG_LEVELS[args.logging_output] + break + else: + custom_level = LOG_LEVELS['default'] + + logging.getLogger(LOG_NAME).setLevel(level) + logging.getLogger(LOG_CUSTOM_NAME).setLevel(custom_level)