#!/usr/bin/env python # # Logging manager singleton. # ####################################################################### import re import sys import os.path import logging import ConfigParser from dm.common.utility import configurationManager from dm.common.exceptions.configurationError import ConfigurationError ####################################################################### # Get singleton instance. def getInstance(): from dm.common.utility.loggingManager import LoggingManager try: lm = LoggingManager() except LoggingManager, ex: lm = ex return lm def getLogger(name='defaultLogger'): """ Convenience function to obtain logger. """ return getInstance().getLogger(name) def setConsoleLogLevel(level): """ Convenience function to set the console log level. """ return getInstance().setConsoleLogLevel(level) def setFileLogLevel(level): """ Convenience function to set the file log level. """ return getInstance().setFileLogLevel(level) class LoggingManager: """ Configuration: The log manager class is initialized via a configuration file that may have the following sections: ConsoleLogging # Used for output on the screen FileLogging # Used for logging into a file Each section in the configuration file should have the following keys: handler # Indicates which handler class to use level # Indicates logging level format # Indicates format for log messages dateformat # Indicates date format used for log messages Given below is an example of a valid configuration file: [ConsoleLogging] handler=ConsoleLoggingHandler(sys.stdout,) level=info format=[%(levelname)s] %(message)s dateformat=%m/%d/%y %H:%M:%S [FileLogging] handler=TimedRotatingFileLoggingHandler('/tmp/dm.log') level=debug format=%(asctime)s,%(msecs)d [%(levelname)s] %(module)s:%(lineno)d %(user)s@%(host)s %(name)s (%(process)d): %(message)s dateformat=%m/%d/%y %H:%M:%S """ # Singleton. __instance = None def __init__(self): if LoggingManager.__instance: raise LoggingManager.__instance LoggingManager.__instance = self self._consoleHandler = None self._fileHandlerList = [] self._maxIntLevel = logging.CRITICAL self._minIntLevel = logging.NOTSET self._levelRegExList = [] self._logger = logging.getLogger(self.__class__.__name__) self._initFlag = False def setMinLogLevel(self, minLogLevel=logging.INFO): self._minIntLevel = minLogLevel def parseLevelRegEx(self, levelRegExList): """ Parse a list of expressions of the form <regex>=<log-level>. """ lines = levelRegExList.split('\n') for line in lines: try: # Use the right split so we can have '='s in the regex (regex, level) = line.rsplit('=', 1) pattern = re.compile(regex) tuple = (pattern, logging.getLevelName(level.upper())) self._levelRegExList.append(tuple) except Exception, ex: # Do not fail, but log an error. self._logger.error('Parser error in log configuration file: %s' % line) self._logger.exception(ex) # Get Log Level based on a string representation def getIntLogLevel(self, levelStr): level = logging.getLevelName(levelStr) # Level should be an integer try: return int(level) except ValueError, ex: raise ConfigurationError('"%s" is not valid log level' % levelStr) # Configure log handlers. def configureHandlers(self): """ Configure log handlers from the config file. """ cm = configurationManager.getInstance() configFile = cm.getLogConfigFile() configSections = self.__getConfigSections(configFile) # Console handler. defaults = { 'level' : cm.getConsoleLogLevel(), 'format' : cm.getLogRecordFormat(), 'dateformat' : cm.getLogDateFormat(), 'handler' : 'ConsoleLoggingHandler(sys.stdout,)' } consoleHandler = self.__configureHandler(configFile, 'ConsoleLogging', defaults) if consoleHandler != None: self._consoleHandler = consoleHandler # File logging. # Do not configure if log directory does # not exist. defaults['handler'] = None defaults['level'] = cm.getFileLogLevel() if not os.path.exists(configFile): # No config file, we'll configure default. defaultLogFile = cm.getLogFile() defaultLogDir = os.path.dirname(defaultLogFile) if os.path.exists(defaultLogDir): handler = 'TimedRotatingFileLoggingHandler("%s")' % defaultLogFile defaults['handler'] = handler fileHandler = self.__configureHandler(configFile, 'FileLogging', defaults) if fileHandler != None: self._fileHandlerList.append(fileHandler) else: # Parse all file loggers present in the config file for configSection in configSections: if configSection.startswith('FileLogging'): fileHandler = self.__configureHandler(configFile, configSection, defaults) if fileHandler != None: self._fileHandlerList.append(fileHandler) # Add handlers to the root logger. Use logging class here # to make sure we can have a logger when we parse the # logger expressions rootLogger = logging.getLogger('') for handler in [self._consoleHandler] + self._fileHandlerList: rootLogger.addHandler(handler) # Get a logger factory based on our current config self.configureLoggers(configFile, defaultLevel=cm.getFileLogLevel()) def configureLoggers(self, configFile, defaultLevel='error'): configParser = ConfigParser.ConfigParser() configParser.read(configFile) rootLogLevel = 'error' levelRegEx = '^.*$=%s' % (defaultLevel) if configParser.has_section('LoggerLevels'): rootLogLevel = configParser.get('LoggerLevels', 'root', rootLogLevel) levelRegEx = configParser.get('LoggerLevels', 'levelregex', levelRegEx) rootLevelInt = logging.getLevelName(rootLogLevel.upper()) logging.getLogger('').root.setLevel(rootLevelInt) logging.getLogger('').debug('Set root logger to %s' % rootLevelInt) if not levelRegEx: return # Parse expressions of the form <regex>=<log-level>. """ lines = levelRegEx.split('\n') for line in lines: try: # Use the right split so we can have '='s in the regex (regex, level) = line.rsplit('=', 1) pattern = re.compile(regex) tuple = (pattern, logging.getLevelName(level.upper())) self._levelRegExList.append(tuple) except Exception, ex: # Do not fail self._logger.error('Parser error in log configuration file: %s' % line) self._logger.exception(ex) def __getOptionFromConfigFile(self, configParser, configSection, key, defaultValue=None): """ Get specified option from the configuration file. """ if configParser.has_section(configSection): return configParser.get(configSection, key, True) else: return defaultValue # Get the sections in the config file def __getConfigSections(self, configFile): """ Return a list of the sections in the given config file """ configParser = ConfigParser.RawConfigParser() configParser.read(configFile) return configParser.sections() # Configure particular handler with given defaults. def __configureHandler(self, configFile, configSection, defaults): """ Configure specified handler with a given defaults. """ configParser = ConfigParser.ConfigParser(defaults) configParser.read(configFile) handlerOption = defaults['handler'] try: handlerOption = configParser.get(configSection, 'handler', True) except Exception, ex: pass # If handlerOption is empty, handler cannot be instantiated. handler = None if handlerOption != None: # Handler argument format: MyHandler(arg1, arg2, ...) # Module will be in lowercase letters, but the class # should be capitalized. handlerName = re.sub('\(.*', '', handlerOption) moduleName = handlerName[0].lower() + handlerName[1:] try: exec 'from dm.common.utility import %s' % (moduleName) exec '_handler = %s.%s' % (moduleName, handlerOption) except IOError, ex: errno, _emsg = ex import errno # If the exception raised is an I/O permissions error, ignore # it and disable this log handler. This allows non-root users # to use the (system-wide) default log configuration if _errno != errno.EACCES: raise _handler = None except Exception, ex: raise ConfigurationError(exception=ex) # Only request setting from the config file if it was # not set via environment variable, or programmatically. if _handler != None: try: _level = self.__getOptionFromConfigFile(configParser, configSection, 'level', defaults['level']) intLevel = self.getIntLogLevel(_level.upper()) _handler.setLevel(intLevel) _format = self.__getOptionFromConfigFile(configParser, configSection, 'format', defaults['format']) _dateformat = self.__getOptionFromConfigFile(configParser, configSection, 'dateformat', defaults['dateformat']) _handler.setFormatter(logging.Formatter(_format, _dateformat)) except Exception, ex: raise ConfigurationError(exception=ex) # Look to see if there is a filter to apply to the handler filter = None try: filter = configParser.get(configSection, 'filter') except Exception, ex: pass if filter: _handler.addFilter(logging.Filter(filter)) return _handler def getLogger(self, name='defaultLogger'): if not self._initFlag: self._initFlag = True self.configureHandlers() logger = logging.getLogger(name) logger.setLevel(self.getLevel(name)) return logger def getLevel(self, name): # Match from the known regex list. level = logging.NOTSET # The last regex is most important. for e in reversed(self._levelRegExList): (pattern, level) = e # If we return not None it is a match if not None == pattern.match(name): break if level > self._maxIntLevel: level = self._maxIntLevel if level < self._minIntLevel: level = self._minIntLevel return level def setConsoleLogLevel(self, level): try: # We need to override the logger levels and the handler intLevel = self.getIntLogLevel(level.upper()) self._consoleHandler.setLevel(intLevel) self._maxIntLevel = intLevel self._logger.setLevel(intLevel) except Exception, ex: raise ConfigurationError(exception=ex) def setFileLogLevel(self, level): try: # We need to override the logger levels and the handler intLevel = self.getIntLogLevel(level.upper()) for handler in self._fileHandlerList: handler.setLevel(intLevel) self._maxIntLevel = intLevel self._logger.setLevel(intLevel) except Exception, ex: raise ConfigurationError(exception=ex) ####################################################################### # Testing. if __name__ == '__main__': lm = getInstance() logger = lm.getLogger('Main') logger.info('Info In Main') logger = lm.getLogger('Main') logger.info('Info In Main 2') logger = lm.getLogger('') logger.info('Info using root logger') logger = lm.getLogger('Main.2') logger.info('Info in Main.2') logger.debug('You should not see this message') lm.setConsoleLogLevel('debug') logger.debug('Debug in Main.2')