Mercurial > libervia-backend
diff src/core/log.py @ 1007:a7d33c7a8277
core (log): refactoring + twisted backend:
- configuration is now done with classes, so inheritance is possible
- twisted backend now use twisted log methods
- log not produced by SàT directly in twisted backend are captured by an observer (twistedObserver) and sent back to SàT logging chain, with the "twisted" logger.
- \\default output use normal twistd output, except in debug mode where it add an stdout output in addition to the log file.
- when log_colors = True, colors are enabled only where it's possible (tty out), when log_colors=force colors are always enabled.
- fixed some bad log.xxx calls
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 05 May 2014 18:58:34 +0200 |
parents | 325fd230c15d |
children | d70d4fe5c5f8 |
line wrap: on
line diff
--- a/src/core/log.py Mon May 05 18:58:34 2014 +0200 +++ b/src/core/log.py Mon May 05 18:58:34 2014 +0200 @@ -27,17 +27,36 @@ _loggers = {} _handlers = {} + class Filtered(Exception): pass + class Logger(object): """High level logging class""" - fmt = None - filter_name = None + fmt = None # format option as given by user (e.g. SAT_LOG_LOGGER) + filter_name = None # filter to call post_treat = None def __init__(self, name): - self._name = name + if isinstance(name, Logger): + self.copy(name) + else: + self._name = name + + def copy(self, other): + """Copy values from other Logger""" + self.fmt = other.fmt + self.Filter_name = other.fmt + self.post_treat = other.post_treat + self._name = other._name + + def out(self, message, level=None): + """Actually log the message + + @param message: formatted message + """ + print message def log(self, level, message): """Print message @@ -48,9 +67,9 @@ try: formatted = self.format(level, message) if self.post_treat is None: - print formatted + self.out(formatted, level) else: - print self.post_treat(level, formatted) + self.out(self.post_treat(level, formatted), level) except Filtered: pass @@ -61,7 +80,7 @@ @param message: message to format @return: formatted message - @raise: Filtered the message must not be logged + @raise: Filtered when the message must not be logged """ if self.fmt is None and self.filter_name is None: return message @@ -75,7 +94,7 @@ except AttributeError: if self.filter_name is not None: raise ValueError("Bad filter: filters must have a .filter method") - return self.fmt % record + return self.fmt % record if self.fmt is not None else message def debug(self, msg): self.log(C.LOG_LVL_DEBUG, msg) @@ -93,6 +112,23 @@ self.log(C.LOG_LVL_CRITICAL, msg) +class TwistedLogger(Logger): + colors = True + force_colors = False + + def __init__(self, *args, **kwargs): + super(TwistedLogger, self).__init__(*args, **kwargs) + from twisted.python import log + self.twisted_log = log + + def out(self, message, level=None): + """Actually log the message + + @param message: formatted message + """ + self.twisted_log.msg(message.encode('utf-8', 'ignore'), sat_logged=True, level=level) + + class FilterName(object): """Filter on logger name according to a regex""" @@ -152,145 +188,376 @@ out = message return ''.join(out) -def _manageOutputs(outputs_raw): - """ Parse output option in a backend agnostic way, and fill _handlers consequently + +class Configure(object): + LOGGER_CLASS = None - @param outputs_raw: output option as enterred in environment variable or in configuration - """ - if not outputs_raw: - return - outputs = outputs_raw.split(C.LOG_OPT_OUTPUT_SEP) - global _handlers - if len(outputs) == 1: - _handlers[C.LOG_OPT_OUTPUT_FILE] = outputs.pop() + def __init__(self, level=None, fmt=None, output=None, logger=None, colors=False, force_colors=False): + """Configure backend + @param level: one of C.LOG_LEVELS + @param fmt: format string, pretty much as in std logging. Accept the following keywords (maybe more depending on backend): + - "message" + - "levelname" + - "name" (logger name) + @param logger: if set, use it as a regular expression to filter on logger name. + Use search to match expression, so ^ or $ can be necessary. + @param colors: if True use ANSI colors to show log levels + @param force_colors: if True ANSI colors are used even if stdout is not a tty + """ + self.preTreatment() + self.configureLevel(level) + self.configureFormat(fmt) + self.configureOutput(output) + self.configureLogger(logger) + self.configureColors(colors, force_colors) + self.postTreatment() + self.updateCurrentLogger() + + def updateCurrentLogger(self): + """update existing logger to the class needed for this backend""" + if self.LOGGER_CLASS is None: + return + for name, logger in _loggers.items(): + _loggers[name] = self.LOGGER_CLASS(logger) + + def preTreatment(self): + pass + + def configureLevel(self, level): + pass - for output in outputs: - if not output: - continue - if output[-1] == ')': - # we have options - opt_begin = output.rfind('(') - options = output[opt_begin+1:-1] - output = output[:opt_begin] - else: - options = None + def configureFormat(self, fmt): + pass + + def configureOutput(self, output): + pass + + def configureLogger(self, logger): + pass + + def configureColors(self, colors, force_colors): + pass + + def postTreatment(self): + pass + + def manageOutputs(self, outputs_raw): + """ Parse output option in a backend agnostic way, and fill _handlers consequently - if output not in (C.LOG_OPT_OUTPUT_DEFAULT, C.LOG_OPT_OUTPUT_FILE, C.LOG_OPT_OUTPUT_MEMORY): - raise ValueError(u"Invalid output [%s]" % output) + @param outputs_raw: output option as enterred in environment variable or in configuration + """ + if not outputs_raw: + return + outputs = outputs_raw.split(C.LOG_OPT_OUTPUT_SEP) + global _handlers + if len(outputs) == 1: + _handlers[C.LOG_OPT_OUTPUT_FILE] = [outputs.pop()] + + for output in outputs: + if not output: + continue + if output[-1] == ')': + # we have options + opt_begin = output.rfind('(') + options = output[opt_begin+1:-1] + output = output[:opt_begin] + else: + options = None + + if output not in (C.LOG_OPT_OUTPUT_DEFAULT, C.LOG_OPT_OUTPUT_FILE, C.LOG_OPT_OUTPUT_MEMORY): + raise ValueError(u"Invalid output [%s]" % output) - if output == C.LOG_OPT_OUTPUT_DEFAULT: - # no option for defaut handler - _handlers[output] = None - elif output == C.LOG_OPT_OUTPUT_FILE: - if not options: - ValueError("%(handler)s output need a path as option" % {'handler': output}) - _handlers[output] = options - options = None # option are parsed, we can empty them - elif output == C.LOG_OPT_OUTPUT_MEMORY: - # we have memory handler, option can be the len limit or None - try: - limit = int(options) + if output == C.LOG_OPT_OUTPUT_DEFAULT: + # no option for defaut handler + _handlers[output] = None + elif output == C.LOG_OPT_OUTPUT_FILE: + if not options: + ValueError("%(handler)s output need a path as option" % {'handler': output}) + _handlers.setdefault(output, []).append(options) options = None # option are parsed, we can empty them - except (TypeError, ValueError): - limit = C.LOG_OPT_OUTPUT_MEMORY_LIMIT - _handlers[output] = limit + elif output == C.LOG_OPT_OUTPUT_MEMORY: + # we have memory handler, option can be the len limit or None + try: + limit = int(options) + options = None # option are parsed, we can empty them + except (TypeError, ValueError): + limit = C.LOG_OPT_OUTPUT_MEMORY_LIMIT + _handlers[output] = limit + + if options: # we should not have unparsed options + raise ValueError(u"options [%(options)s] are not supported for %(handler)s output" % {'options': options, 'handler': output}) + - if options: # we should not have unparsed options - raise ValueError(u"options [%(options)s] are not supported for %(handler)s output" % {'options': options, 'handler': output}) +class ConfigureBasic(Configure): + def configureLevel(self, level): + if level is not None: + # we deactivate methods below level + level_idx = C.LOG_LEVELS.index(level) + def dev_null(self, msg): + pass + for _level in C.LOG_LEVELS[:level_idx]: + setattr(Logger, _level.lower(), dev_null) + + def configureFormat(self, fmt): + if fmt is not None: + if fmt != '%(message)s': # %(message)s is the same as None + Logger.fmt = fmt + + def configureOutput(self, output): + if output is not None: + if output != C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT: + # TODO: manage other outputs + raise NotImplementedError("Basic backend only manage default output yet") + + def configureLogger(self, logger): + if logger: + Logger.filter_name = FilterName(logger) -def _configureStdLogging(logging, level=None, fmt=C.LOG_OPT_FORMAT[1], output=C.LOG_OPT_OUTPUT[1], logger=None, colors=False, force_colors=False): - """Configure standard logging module + def configureColors(self, colors, force_colors): + if colors: + import sys + if force_colors or sys.stdout.isatty(): # FIXME: isatty should be tested on each handler, not globaly + # we need colors + Logger.post_treat = lambda self, level, message: _ansiColors(level, message) + elif force_colors: + raise ValueError("force_colors can't be used if colors is False") + + +class ConfigureTwisted(ConfigureBasic): + LOGGER_CLASS = TwistedLogger + + def changeObserver(self, observer, can_color=False): + """Install a hook on observer to manage SàT specificities - @param logging: standard logging module - @param level: one of C.LOG_LEVELS - @param fmt: format string, pretty much as in std logging. Accept the following keywords (maybe more depending on backend): - - "message" - - "levelname" - - "name" (logger name) - @param logger: if set, use it as a regular expression to filter on logger name. - Use search to match expression, so ^ or $ can be necessary. - @param colors: if True use ANSI colors to show log levels - @param force_colors: if True ANSI colors are used even if stdout is not a tty - """ - format_ = fmt - if level is None: - level = C.LOG_LVL_DEBUG - import sys - with_color = colors & (sys.stdout.isatty() or force_colors) - if not colors and force_colors: - raise ValueError("force_colors can't be used if colors is False") + @param observer: original observer to hook + @param can_color: True if observer can display ansi colors + """ + def observer_hook(event): + """redirect non SàT log to twisted_logger, and add colors when possible""" + if 'sat_logged' in event: # we only want our own logs, other are managed by twistedObserver + # we add colors if possible + if (can_color and self.LOGGER_CLASS.colors) or self.LOGGER_CLASS.force_colors: + message = event.get('message', tuple()) + level = event.get('level', C.LOG_LVL_INFO) + if message: + event['message'] = (_ansiColors(level, ''.join(message)),) # must be a tuple + observer(event) # we can now call the original observer + + return observer_hook + + def changeFileLogObserver(self, observer): + """Install SàT hook for FileLogObserver + + if the output is a tty, we allow colors, else we don't + @param observer: original observer to hook + """ + log_obs = observer.__self__ + log_file = log_obs.write.__self__ + try: + can_color = log_file.isatty() + except AttributeError: + can_color = False + return self.changeObserver(observer, can_color=can_color) + + def installObserverHook(self, observer): + """Check observer type and install SàT hook when possible - class SatFormatter(logging.Formatter): - u"""Formatter which manage SàT specificities""" - - def __init__(self, fmt=None, datefmt=None): - super(SatFormatter, self).__init__(fmt, datefmt) - - def format(self, record): - s = super(SatFormatter, self).format(record) - if with_color: - s = _ansiColors(record.levelname, s) - return s + @param observer: observer to hook + @return: hooked observer or original one + """ + if hasattr(observer, '__self__'): + ori = observer + if isinstance(observer.__self__, self.log.FileLogObserver): + observer = self.changeFileLogObserver(observer) + elif isinstance(observer.__self__, self.log.DefaultObserver): + observer = self.changeObserver(observer, can_color=True) + else: + # we use print because log system is not fully initialized + print("Unmanaged observer [%s]" % observer) + return observer + self.observers[ori] = observer + return observer - root_logger = logging.getLogger() - if len(root_logger.handlers) == 0: - _manageOutputs(output) - formatter = SatFormatter(format_) - name_filter = FilterName(logger) if logger else None - for handler, options in _handlers.items(): - if handler == C.LOG_OPT_OUTPUT_DEFAULT: - hdlr = logging.StreamHandler() - elif handler == C.LOG_OPT_OUTPUT_MEMORY: - import logging.handlers - hdlr = logging.handlers.BufferingHandler(options) - _handlers[handler] = hdlr # we keep a reference to the handler to read the buffer later - elif handler == C.LOG_OPT_OUTPUT_FILE: - import os.path - hdlr = logging.FileHandler(os.path.expanduser(options)) + def preTreatment(self): + """initialise needed attributes, and install observers hooks""" + self.observers = {} + from twisted.python import log + self.log = log + self.log_publisher = log.msg.__self__ + def addObserverObserver(self_logpub, other): + """Install hook so we know when a new observer is added""" + other = self.installObserverHook(other) + return self_logpub._originalAddObserver(other) + def removeObserverObserver(self_logpub, ori): + """removeObserver hook fix + + As we wrap the original observer, the original removeObserver may want to remove the original object instead of the wrapper, this method fix this + """ + if ori in self.observers: + self_logpub._originalRemoveObserver(self.observers[ori]) else: - raise ValueError("Unknown handler type") - hdlr.setFormatter(formatter) - root_logger.addHandler(hdlr) - root_logger.setLevel(level) - if name_filter is not None: - hdlr.addFilter(name_filter) - else: - root_logger.warning(u"Handlers already set on root logger") + try: + self_logpub._originalRemoveObserver(ori) + except ValueError: + try: + ori in self.cleared_observers + except AttributeError: + raise ValueError("Unknown observer") + + # we replace addObserver/removeObserver by our own + log.LogPublisher._originalAddObserver = log.LogPublisher.addObserver + log.LogPublisher._originalRemoveObserver = log.LogPublisher.removeObserver + import types # see https://stackoverflow.com/a/4267590 (thx Chris Morgan/aaronasterling) + log.addObserver = types.MethodType(addObserverObserver, self.log_publisher, log.LogPublisher) + log.removeObserver = types.MethodType(removeObserverObserver, self.log_publisher, log.LogPublisher) + + # we now change existing observers + for idx, observer in enumerate(self.log_publisher.observers): + self.log_publisher.observers[idx] = self.installObserverHook(observer) + + + def configureLevel(self, level): + self.LOGGER_CLASS.level = level + super(ConfigureTwisted, self).configureLevel(level) + + def configureOutput(self, output): + import sys + if output is None: + output = C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT + self.manageOutputs(output) + addObserver = self.log.addObserver + + if C.LOG_OPT_OUTPUT_DEFAULT in _handlers: + # default output is already managed, we just add output to stdout if we are in debug mode + from twisted.internet import defer + if defer.Deferred.debug: + addObserver(self.log.FileLogObserver(sys.stdout).emit) + else: + # \\default is not in the output, so we remove current observers + self.cleared_observers = self.log_publisher.observers + self.observers.clear() + del self.log_publisher.observers[:] + # and we forbid twistd to add any observer + self.log.addObserver = lambda other: None + + if C.LOG_OPT_OUTPUT_FILE in _handlers: + from twisted.python import logfile + for path in _handlers[C.LOG_OPT_OUTPUT_FILE]: + log_file = sys.stdout if path == '-' else logfile.LogFile.fromFullPath(path) + addObserver(self.log.FileLogObserver(log_file).emit) + + if C.LOG_OPT_OUTPUT_MEMORY in _handlers: + raise NotImplementedError("Memory observer is not implemented in Twisted backend") + + def configureColors(self, colors, force_colors): + self.LOGGER_CLASS.colors = colors + self.LOGGER_CLASS.force_colors = force_colors + if force_colors and not colors: + raise ValueError('colors must be True if force_colors is True') + + def postTreatment(self): + """Install twistedObserver which manage non SàT logs""" + def twistedObserver(event): + """Observer which redirect log message not produced by SàT to SàT logging system""" + if not 'sat_logged' in event: + # this log was not produced by SàT + from twisted.python import log + text = log.textFromEventDict(event) + if text is None: + return + twisted_logger = getLogger(C.LOG_TWISTED_LOGGER) + log_method = twisted_logger.error if event.get('isError', False) else twisted_logger.info + log_method(text.decode('utf-8')) + + self.log_publisher._originalAddObserver(twistedObserver) + + +class ConfigureStandard(Configure): + + def __init__(self, level=None, fmt=C.LOG_OPT_FORMAT[1], output=C.LOG_OPT_OUTPUT[1], logger=None, colors=False, force_colors=False): + super(ConfigureStandard, self).__init__(level, fmt, output, logger, colors, force_colors) -def _configureBasic(level=None, fmt=None, output=None, logger=None, colors=False, force_colors=False): - """Configure basic backend - @param level: same as _configureStdLogging.level - @param fmt: same as _configureStdLogging.fmt - @param output: not implemented yet TODO - @param logger: same as _configureStdLogging.logger - @param colors: same as _configureStdLogging.colors - @param force_colors: same as _configureStdLogging.force_colors - """ - if level is not None: - # we deactivate methods below level - level_idx = C.LOG_LEVELS.index(level) - def dev_null(self, msg): - pass - for _level in C.LOG_LEVELS[:level_idx]: - setattr(Logger, _level.lower(), dev_null) - if fmt is not None: - if fmt != '%(message)s': # %(message)s is the same as None - Logger.fmt = fmt - if output is not None: - if output != C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT: - # TODO: manage other outputs - raise NotImplementedError("Basic backend only manage default output yet") + def preTreatment(self): + """We use logging methods directly, instead of using Logger""" + global getLogger + global debug + global info + global warning + global error + global critical + import logging + getLogger = logging.getLogger + debug = logging.debug + info = logging.info + warning = logging.warning + error = logging.error + critical = logging.critical + + def configureLevel(self, level): + if level is None: + level = C.LOG_LVL_DEBUG + self.level = level + + def configureFormat(self, fmt): + import logging + format_ = fmt + + class SatFormatter(logging.Formatter): + u"""Formatter which manage SàT specificities""" + + def __init__(self, fmt=None, datefmt=None): + super(SatFormatter, self).__init__(fmt, datefmt) + + def format(self, record): + s = super(SatFormatter, self).format(record) + if self.with_color: + s = _ansiColors(record.levelname, s) + return s + + self.formatter = SatFormatter(format_) - if logger: - Logger.filter_name = FilterName(logger) + def configureOutput(self, output): + self.manageOutputs(output) - if colors: + def configureLogger(self, logger): + self.name_filter = FilterName(logger) if logger else None + + def configureColors(self, colors, force_colors): import sys - if force_colors or sys.stdout.isatty(): - # we need colors - Logger.post_treat = lambda self, level, message: _ansiColors(level, message) - elif force_colors: - raise ValueError("force_colors can't be used if colors is False") + self.with_color = colors & (sys.stdout.isatty() or force_colors) + if not colors and force_colors: + raise ValueError("force_colors can't be used if colors is False") + + def _addHandler(self, root_logger, hdlr): + hdlr.setFormatter(self.formatter) + root_logger.addHandler(hdlr) + root_logger.setLevel(self.level) + if self.name_filter is not None: + hdlr.addFilter(self.name_filter) + + def postTreatment(self): + import logging + root_logger = logging.getLogger() + if len(root_logger.handlers) == 0: + for handler, options in _handlers.items(): + if handler == C.LOG_OPT_OUTPUT_DEFAULT: + hdlr = logging.StreamHandler() + self._addHandler(root_logger, hdlr) + elif handler == C.LOG_OPT_OUTPUT_MEMORY: + import logging.handlers + hdlr = logging.handlers.BufferingHandler(options) + _handlers[handler] = hdlr # we keep a reference to the handler to read the buffer later + self._addHandler(root_logger, hdlr) + elif handler == C.LOG_OPT_OUTPUT_FILE: + import os.path + for path in options: + hdlr = logging.FileHandler(os.path.expanduser(path)) + self._addHandler(root_logger, hdlr) + else: + raise ValueError("Unknown handler type") + else: + root_logger.warning(u"Handlers already set on root logger") + def configure(backend=C.LOG_BACKEND_STANDARD, **options): """Configure logging behaviour @@ -304,28 +571,14 @@ raise exceptions.InternalError("Logging can only be configured once") _backend = backend - if backend in (C.LOG_BACKEND_TWISTED, C.LOG_BACKEND_STANDARD): - if backend == C.LOG_BACKEND_TWISTED: - from twisted.python import log - observer = log.PythonLoggingObserver() - observer.start() - global getLogger - global debug - global info - global warning - global error - global critical - import logging - _configureStdLogging(logging, **options) - getLogger = logging.getLogger - debug = logging.debug - info = logging.info - warning = logging.warning - error = logging.error - critical = logging.critical + if backend == C.LOG_BACKEND_BASIC: + ConfigureBasic(**options) - elif backend == C.LOG_BACKEND_BASIC: - _configureBasic(**options) + elif backend == C.LOG_BACKEND_TWISTED: + ConfigureTwisted(**options) + + elif backend == C.LOG_BACKEND_STANDARD: + ConfigureStandard(**options) else: raise ValueError("unknown backend") @@ -375,7 +628,13 @@ configure(backend, **log_conf) def getLogger(name=C.LOG_BASE_LOGGER): - return _loggers.setdefault(name, Logger(name)) + if _backend in (None, C.LOG_BACKEND_BASIC): + logger_class = Logger + elif _backend == C.LOG_BACKEND_TWISTED: + logger_class = TwistedLogger + else: + raise ValueError("This method should not be called with backend [%s]" % _backend) + return _loggers.setdefault(name, logger_class(name)) _root_logger = getLogger()