From 782dca216c30a499adcdfe5933bec87a2bea87ee Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Wed, 7 Feb 2018 14:04:19 -0800 Subject: [PATCH] Add default logging configuration Add a default logging configuration based on what was recently added to Zuul. This will use sane defaults for system logging, though it does not yet address image build logging. Change-Id: Ie86fc6d6839e2eb199c27515346b57b2ebede703 --- nodepool/cmd/__init__.py | 31 ++++---- nodepool/logconfig.py | 153 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 nodepool/logconfig.py diff --git a/nodepool/cmd/__init__.py b/nodepool/cmd/__init__.py index 8ccf480dc..036514a02 100644 --- a/nodepool/cmd/__init__.py +++ b/nodepool/cmd/__init__.py @@ -26,10 +26,8 @@ import sys import threading import traceback -import yaml - from nodepool.version import version_info as npd_version_info - +from nodepool import logconfig # as of python-daemon 1.6 it doesn't bundle pidlockfile anymore # instead it depends on lockfile-0.9.1 which uses pidfile. @@ -103,21 +101,20 @@ class NodepoolApp(object): def setup_logging(self): if self.args.logconfig: fp = os.path.expanduser(self.args.logconfig) - - if not os.path.exists(fp): - m = "Unable to read logging config file at %s" % fp - raise Exception(m) - - if os.path.splitext(fp)[1] in ('.yml', '.yaml'): - with open(fp, 'r') as f: - logging.config.dictConfig(yaml.safe_load(f)) - - else: - logging.config.fileConfig(fp) - + logging_config = logconfig.load_config(fp) else: - m = '%(asctime)s %(levelname)s %(name)s: %(message)s' - logging.basicConfig(level=logging.DEBUG, format=m) + # If someone runs in the foreground and doesn't give a logging + # config, leave the config set to emit to stdout. + if hasattr(self.args, 'nodaemon') and self.args.nodaemon: + logging_config = logconfig.ServerLoggingConfig() + logging_config.setDebug() + else: + # Setting a server value updates the defaults to use + # WatchedFileHandler on /var/log/nodepool/{server}-debug.log + # and /var/log/nodepool/{server}.log + logging_config = logconfig.ServerLoggingConfig( + server=self.app_name) + logging_config.apply() def _main(self, argv=None): if argv is None: diff --git a/nodepool/logconfig.py b/nodepool/logconfig.py new file mode 100644 index 000000000..1ac32291c --- /dev/null +++ b/nodepool/logconfig.py @@ -0,0 +1,153 @@ +# Copyright 2017 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import copy +import logging.config +import json +import os + +import yaml + + +_DEFAULT_SERVER_LOGGING_CONFIG = { + 'version': 1, + 'formatters': { + 'simple': { + 'format': '%(asctime)s %(levelname)s %(name)s: %(message)s' + }, + }, + 'handlers': { + 'console': { + # Used for printing to stdout + 'class': 'logging.StreamHandler', + 'stream': 'ext://sys.stdout', + 'level': 'INFO', + 'formatter': 'simple', + }, + }, + 'loggers': { + 'nodepool': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'requests': { + 'handlers': ['console'], + 'level': 'WARN', + }, + 'shade': { + 'handlers': ['console'], + 'level': 'WARN', + }, + 'kazoo': { + 'handlers': ['console'], + 'level': 'WARN', + }, + }, + 'root': {'handlers': []}, +} + +_DEFAULT_SERVER_FILE_HANDLERS = { + 'normal': { + # Used for writing normal log files + 'class': 'logging.handlers.WatchedFileHandler', + # This will get set to something real by DictLoggingConfig.server + 'filename': '/var/log/nodepool/{server}.log', + 'level': 'INFO', + 'formatter': 'simple', + }, +} + + +def _read_config_file(filename: str): + if not os.path.exists(filename): + raise ValueError("Unable to read logging config file at %s" % filename) + + if os.path.splitext(filename)[1] in ('.yml', '.yaml', '.json'): + return yaml.safe_load(open(filename, 'r')) + return filename + + +def load_config(filename: str): + config = _read_config_file(filename) + if isinstance(config, dict): + return DictLoggingConfig(config) + return FileLoggingConfig(filename) + + +class LoggingConfig(object, metaclass=abc.ABCMeta): + + @abc.abstractmethod + def apply(self): + """Apply the config information to the current logging config.""" + + +class DictLoggingConfig(LoggingConfig, metaclass=abc.ABCMeta): + + def __init__(self, config): + self._config = config + + def apply(self): + logging.config.dictConfig(self._config) + + def writeJson(self, filename: str): + with open(filename, 'w') as f: + f.write(json.dumps(self._config, indent=2)) + + +class ServerLoggingConfig(DictLoggingConfig): + + def __init__(self, config=None, server=None): + if not config: + config = copy.deepcopy(_DEFAULT_SERVER_LOGGING_CONFIG) + super(ServerLoggingConfig, self).__init__(config=config) + if server: + self.server = server + + @property + def server(self): + return self._server + + @server.setter + def server(self, server): + self._server = server + # Add the normal file handler. It's not included in the default + # config above because we're templating out the filename. Also, we + # only want to add the handler if we're actually going to use it. + for name, handler in _DEFAULT_SERVER_FILE_HANDLERS.items(): + server_handler = copy.deepcopy(handler) + server_handler['filename'] = server_handler['filename'].format( + server=server) + self._config['handlers'][name] = server_handler + # Change everything configured to write to stdout to write to + # log files instead. + for logger in self._config['loggers'].values(): + if logger['handlers'] == ['console']: + logger['handlers'] = ['normal'] + + def setDebug(self): + # Change level from INFO to DEBUG + for section in ('handlers', 'loggers'): + for handler in self._config[section].values(): + if handler.get('level') == 'INFO': + handler['level'] = 'DEBUG' + + +class FileLoggingConfig(LoggingConfig): + + def __init__(self, filename): + self._filename = filename + + def apply(self): + logging.config.fileConfig(self._filename)