changeset 1006:325fd230c15d

core (log): added advanced feature to basic backend (colors/formatting/level and logger filtering)
author Goffi <goffi@goffi.org>
date Mon, 05 May 2014 18:58:34 +0200
parents b4af31a8a4f2
children a7d33c7a8277
files src/core/log.py
diffstat 1 files changed, 132 insertions(+), 31 deletions(-) [+]
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,30 +27,74 @@
 _loggers = {}
 _handlers = {}
 
+class Filtered(Exception):
+    pass
 
 class Logger(object):
-    """ High level logging class """
+    """High level logging class"""
+    fmt = None
+    filter_name = None
+    post_treat = None
 
     def __init__(self, name):
         self._name = name
 
+    def log(self, level, message):
+        """Print message
+
+        @param level: one of C.LOG_LEVELS
+        @param message: message to format and print
+        """
+        try:
+            formatted = self.format(level, message)
+            if self.post_treat is None:
+                print formatted
+            else:
+                print self.post_treat(level, formatted)
+        except Filtered:
+            pass
+
+    def format(self, level, message):
+        """Format message according to Logger.fmt
+
+        @param level: one of C.LOG_LEVELS
+        @param message: message to format
+        @return: formatted message
+
+        @raise: Filtered the message must not be logged
+        """
+        if self.fmt is None and self.filter_name is None:
+            return message
+        record = {'name': self._name,
+                  'message': message,
+                  'levelname': level,
+                 }
+        try:
+            if not self.filter_name.dictFilter(record):
+                raise Filtered
+        except AttributeError:
+            if self.filter_name is not None:
+                raise ValueError("Bad filter: filters must have a .filter method")
+        return self.fmt % record
+
     def debug(self, msg):
-        print msg
+        self.log(C.LOG_LVL_DEBUG, msg)
 
     def info(self, msg):
-        print msg
+        self.log(C.LOG_LVL_INFO, msg)
 
     def warning(self, msg):
-        print msg
+        self.log(C.LOG_LVL_WARNING, msg)
 
     def error(self, msg):
-        print msg
+        self.log(C.LOG_LVL_ERROR, msg)
 
     def critical(self, msg):
-        print msg
+        self.log(C.LOG_LVL_CRITICAL, msg)
 
 
 class FilterName(object):
+    """Filter on logger name according to a regex"""
 
     def __init__(self, name_re):
         """Initialise name filter
@@ -66,8 +110,50 @@
             return 1
         return 0
 
+    def dictFilter(self, dict_record):
+        """Filter using a dictionary record
+
+        @param dict_record: dictionary with at list a key "name" with logger name
+        @return: True if message should be logged
+        """
+        class LogRecord(object):
+            pass
+        log_record = LogRecord()
+        log_record.name = dict_record['name']
+        return self.filter(log_record) == 1
+
+
+def _ansiColors(level, message):
+    """Colorise message depending on level for terminals
+
+    @param level: one of C.LOG_LEVELS
+    @param message: formatted message to log
+    @return: message with ANSI escape codes for coloration
+    """
+    if level == C.LOG_LVL_DEBUG:
+        out = (C.ANSI_FG_CYAN, message, C.ANSI_RESET)
+    elif level == C.LOG_LVL_WARNING:
+        out = (C.ANSI_FG_YELLOW, message, C.ANSI_RESET)
+    elif level == C.LOG_LVL_ERROR:
+        out = (C.ANSI_FG_RED,
+               C.ANSI_BLINK,
+               r'/!\ ',
+               C.ANSI_BLINK_OFF,
+               message,
+               C.ANSI_RESET)
+    elif level == C.LOG_LVL_CRITICAL:
+        out = (C.ANSI_BOLD,
+               C.ANSI_FG_RED,
+               'Guru Meditation ',
+               C.ANSI_NORMAL_WEIGHT,
+               message,
+               C.ANSI_RESET)
+    else:
+        out = message
+    return ''.join(out)
+
 def _manageOutputs(outputs_raw):
-    """ Parse output option in a backend agnostic way, and fill _backend consequently
+    """ Parse output option in a backend agnostic way, and fill _handlers consequently
 
     @param outputs_raw: output option as enterred in environment variable or in configuration
     """
@@ -143,28 +229,7 @@
         def format(self, record):
             s = super(SatFormatter, self).format(record)
             if with_color:
-                if record.levelno == logging.DEBUG:
-                    fmt = (C.ANSI_FG_CYAN, s, C.ANSI_RESET)
-                elif record.levelno == logging.WARNING:
-                    fmt = (C.ANSI_FG_YELLOW, s, C.ANSI_RESET)
-                elif record.levelno == logging.ERROR:
-                    fmt = (C.ANSI_FG_RED,
-                           C.ANSI_BLINK,
-                           r'/!\ ',
-                           C.ANSI_BLINK_OFF,
-                           s,
-                           C.ANSI_RESET)
-                elif record.levelno == logging.CRITICAL:
-                    fmt = (C.ANSI_BOLD,
-                           C.ANSI_FG_RED,
-                           'Guru Meditation ',
-                           C.ANSI_NORMAL_WEIGHT,
-                           s,
-                           C.ANSI_RESET)
-                else:
-                    fmt = s
-                s = ''.join(fmt)
-
+                s = _ansiColors(record.levelname, s)
             return s
 
     root_logger = logging.getLogger()
@@ -178,6 +243,7 @@
             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))
@@ -191,8 +257,43 @@
     else:
         root_logger.warning(u"Handlers already set on root logger")
 
+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")
+
+    if logger:
+        Logger.filter_name = FilterName(logger)
+
+    if 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")
+
 def configure(backend=C.LOG_BACKEND_STANDARD, **options):
-    """Configure logging bejaviour
+    """Configure logging behaviour
     @param backend: can be:
         C.LOG_BACKEND_STANDARD: use standard logging module
         C.LOG_BACKEND_TWISTED: use twisted logging module (with standard logging observer)
@@ -224,7 +325,7 @@
         critical = logging.critical
 
     elif backend == C.LOG_BACKEND_BASIC:
-        pass
+        _configureBasic(**options)
 
     else:
         raise ValueError("unknown backend")