Source code for prestoadmin.util.application

# -*- coding: utf-8 -*-
#
# 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.

"""Logic at the application level for logging and exception handling"""

import functools
import logging
import logging.config
import os
import sys
import traceback

import __main__ as main

from prestoadmin import __version__
from prestoadmin.util import constants
from prestoadmin.util import filesystem
from prestoadmin.util.exception import ExceptionWithCause
from prestoadmin.util.local_config_util import get_log_directory

# Normally this would use the logger for __name__, however, this is
# effectively the "root" logger for the application.  If this code
# were running directly in the executable script __name__ would be
# set to '__main__', so we emulate that same behavior here.  This should
# resolve to the same logger that will be used by the entry point script.
logger = logging.getLogger('__main__')


[docs]class Application(object): """ A generic application entry point. Provides logging and exception handling features. This class is expected to be used as a base class for various applications. Parameters: name - human readable name for the application version - the version of the application, as a string log_file_path - optional name of the log file including whatever extension you may want to use. For example, 'foo.log' would create a file called 'foo.log' in the default presto-admin logging directory tree. """ def __init__(self, name, version=None, log_file_path=None): self.name = str(name) self.__log_file_path = log_file_path or (self.name + '.log') if not os.path.isabs(self.__log_file_path): self.__log_file_path = os.path.join( get_log_directory(), self.__log_file_path ) self.version = version or __version__ def __enter__(self): self.__configure_logging() return self def __configure_logging(self): try: for maybe_file_path in self.__logging_configuration_file_paths(): if not os.path.exists(maybe_file_path): continue else: config_file_path = maybe_file_path filesystem.ensure_parent_directories_exist( self.__log_file_path ) logging.config.fileConfig( config_file_path, defaults={'log_file_path': self.__log_file_path}, disable_existing_loggers=False ) self.__log_application_start() logger.debug( 'Loaded logging configuration from %s', config_file_path ) break except Exception as e: sys.stderr.write( 'Please run %s with sudo.\n' % self.name ) sys.stderr.flush() sys.exit(str(e)) def __logging_configuration_file_paths(self): # Current working directory yield constants.LOGGING_CONFIG_FILE_NAME # Application specific yield (self.__log_file_path + '.ini') yield (self.__main_module_path() + '.ini') # Global locations for dir_path in constants.LOGGING_CONFIG_FILE_DIRECTORIES: yield os.path.join(dir_path, constants.LOGGING_CONFIG_FILE_NAME) def __main_module_path(self): return os.path.abspath(main.__file__) def __log_application_start(self): LOG_SEPARATOR = '**************************************************' logger.debug(LOG_SEPARATOR) logger.debug( 'Starting {name} {version}'.format( name=self.name, version=self.version ) ) logger.debug(LOG_SEPARATOR) logger.debug('raw arguments = {0}'.format(sys.argv)) def __exit__(self, exc_type, exception, trace): self.exc_type = exc_type self.exception = exception self.trace = trace try: if exc_type is None: self.__handle_no_exception() elif exc_type == SystemExit: self.__handle_system_exit() else: self._handle_error() sys.exit(1) finally: self._exit_cleanup_hook() def _exit_cleanup_hook(self): logging.shutdown() def __handle_no_exception(self): logger.debug('Exiting normally') def __handle_system_exit(self): # Unfortunately a SystemExit can be raised with all kinds of # wonky values. This code attempts to determine the actual # exit status. code = None try: # according to the docs a None value for this is equivalent # to a 0 value. if self.exception is None or self.exception.code is None: code = 0 else: code = int(self.exception.code) except ValueError: code = 1 except AttributeError: # In Python 2.6, the exceptions are passed as strings sometimes. # Thus exception.code gets an AttributeError. try: code = int(self.exception) except ValueError: code = 1 except: logger.exception("Unknown exception: %s" % str(self.exception)) if code is not None: if code is not 0: self._log_exception() logger.debug('Application exiting with status %d', code) else: self._log_exception() sys.exit(code) def _handle_error(self): self._log_exception() self.__display_error_message(str(self.exception)) def __display_error_message(self, message): log_file_path = self.__get_root_log_file_path() error_message = '' if log_file_path: error_message += ' More detailed information can be found in ' error_message += log_file_path print >> sys.stderr, message + error_message def __get_root_log_file_path(self): for handler in logging.root.handlers: if isinstance(handler, logging.FileHandler): return handler.baseFilename return None def _log_exception(self): formatted_stack_trace = ''.join( traceback.format_exception( self.exc_type, self.exception, self.trace ) + [ExceptionWithCause.get_cause_if_supported(self.exception)] ) logger.error( 'Handling uncaught exception: {t}, "{ex}"\n{tb}'.format( t=self.exc_type, ex=str(self.exception), tb=formatted_stack_trace ) )
[docs]def entry_point(name, version=None, log_file_path=None, application_class=Application): """ A decorator for application entry points. The decorated function will be wrapped in an Application object and executed in that safe environment. Note that decorating a function with this decorator will not actually cause it to be invoked. You must explicitly call the function in the script. Parameters: name - human readable name for the application version - the version of the application, as a string log_file_path - optional name of the log file including whatever extension you may want to use. For example, 'foo.log' would create a file called 'foo.log' in the default prestoadmin logging directory tree. application_class - Type of application to run. The default is Application but there can be subclasses of that class. """ def application_decorator(method): @functools.wraps(method) def wrapped_application(*args, **kwargs): with application_class( name, version=version, log_file_path=log_file_path ): return method(*args, **kwargs) return wrapped_application return application_decorator