Autocomplete working in query_editor plugin
[unfold.git] / manifold / util / log.py
diff --git a/manifold/util/log.py b/manifold/util/log.py
new file mode 100644 (file)
index 0000000..cdb187f
--- /dev/null
@@ -0,0 +1,288 @@
+import sys, logging, traceback, inspect, os.path
+from logging                 import handlers
+from manifold.util.singleton import Singleton
+from manifold.util.options   import Options
+from manifold.util.misc      import caller_name, make_list
+from manifold.util           import colors
+
+# TODO Log should take separately message strings and arguments to be able to
+# remember which messages are seen several times, and also to allow for
+# translation
+# TODO How to log to stdout without putting None in self.log
+
+class Log(object):
+    __metaclass__ = Singleton
+
+    DEFAULTS = {
+        # Logging
+        "rsyslog_enable"      : False,
+        "rsyslog_host"        : None, #"log.top-hat.info",
+        "rsyslog_port"        : None, #28514,
+        "log_file"            : "/var/log/manifold.log",
+        "log_level"           : "DEBUG",
+        "debug"               : "default",
+        "log_duplicates"      : False
+    }
+
+    # COLORS
+    color_ansi = {
+        'DEBUG'  : colors.MYGREEN,
+        'INFO'   : colors.MYBLUE,
+        'WARNING': colors.MYWARNING,
+        'ERROR'  : colors.MYRED,
+        'HEADER' : colors.MYHEADER,
+        'END'    : colors.MYEND,
+        'RECORD' : colors.MYBLUE,
+        'TMP'    : colors.MYRED,
+    }
+
+    @classmethod
+    def color(cls, color):
+        return cls.color_ansi[color] if color else ''
+
+    # To remove duplicate messages
+    seen = {}
+
+    def __init__(self, name='(default)'):
+        self.log = None # logging.getLogger(name)
+        self.files_to_keep = []
+        self.init_log()
+        self.color = True
+
+
+    @classmethod
+    def init_options(self):
+        opt = Options()
+
+        opt.add_option(
+            "--rsyslog-enable", action = "store_false", dest = "rsyslog_enable",
+            help = "Specify if log have to be written to a rsyslog server.",
+            default = self.DEFAULTS["rsyslog_enable"]
+        )
+        opt.add_option(
+            "--rsyslog-host", dest = "rsyslog_host",
+            help = "Rsyslog hostname.",
+            default = self.DEFAULTS["rsyslog_host"]
+        )
+        opt.add_option(
+            "--rsyslog-port", type = "int", dest = "rsyslog_port",
+            help = "Rsyslog port.",
+            default = self.DEFAULTS["rsyslog_port"]
+        )
+        opt.add_option(
+            "-o", "--log-file", dest = "log_file",
+            help = "Log filename.",
+            default = self.DEFAULTS["log_file"]
+        )
+        opt.add_option(
+            "-L", "--log-level", dest = "log_level",
+            choices = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
+            help = "Log level",
+            default = self.DEFAULTS["log_level"]
+        )
+        opt.add_option(
+            "-d", "--debug", dest = "debug",
+            help = "Debug paths (a list of coma-separated python path: path.to.module.function).",
+            default = self.DEFAULTS["debug"]
+        )
+        opt.add_option(
+            "", "--log_duplicates", action = "store_true", dest = "log_duplicates",
+            help = "Remove duplicate messages in logs",
+            default = self.DEFAULTS["log_duplicates"]
+        )
+
+    def init_log(self, options=object()):
+        # Initialize self.log (require self.files_to_keep)
+        if self.log: # for debugging by using stdout, log may be equal to None
+            if Options().rsyslog_host:
+                shandler = self.make_handler_rsyslog(
+                    Options().rsyslog_host,
+                    Options().rsyslog_port,
+                    Options().log_level
+                )
+            elif Options().log_file:
+                shandler = self.make_handler_locallog(
+                    Options().log_file,
+                    Options().log_level
+                )
+
+    #------------------------------------------------------------------------
+    # Log
+    #------------------------------------------------------------------------
+
+    def make_handler_rsyslog(self, rsyslog_host, rsyslog_port, log_level):
+        """
+        \brief (Internal usage) Prepare logging via rsyslog
+        \param rsyslog_host The hostname of the rsyslog server
+        \param rsyslog_port The port of the rsyslog server
+        \param log_level Log level
+        """
+        # Prepare the handler
+        shandler = handlers.SysLogHandler(
+            (rsyslog_host, rsyslog_port),
+            facility = handlers.SysLogHandler.LOG_DAEMON
+        )
+
+        # The log file must remain open while daemonizing 
+        self.prepare_handler(shandler, log_level)
+        return shandler
+
+    def make_handler_locallog(self, log_filename, log_level):
+        """
+        \brief (Internal usage) Prepare local logging
+        \param log_filename The file in which we write the logs
+        \param log_level Log level
+        """
+        # Create directory in which we store the log file
+        log_dir = os.path.dirname(log_filename)
+        if log_dir and not os.path.exists(log_dir):
+            try:
+                os.makedirs(log_dir)
+            except OSError, why:
+                # XXX here we don't log since log is not initialized yet
+                print "OS error: %s" % why
+
+        # Prepare the handler
+        shandler = logging.handlers.RotatingFileHandler(
+            log_filename,
+            backupCount = 0
+        )
+
+        # The log file must remain open while daemonizing 
+        self.files_to_keep.append(shandler.stream)
+        self.prepare_handler(shandler, log_level)
+        return shandler
+
+    def prepare_handler(self, shandler, log_level):
+        """
+        \brief (Internal usage)
+        \param shandler Handler used to log information
+        \param log_level Log level
+        """
+        shandler.setLevel(log_level)
+        formatter = logging.Formatter("%(asctime)s: %(name)s: %(levelname)s %(message)s")
+        shandler.setFormatter(formatter)
+        self.log.addHandler(shandler)
+        self.log.setLevel(getattr(logging, log_level, logging.INFO))
+                      
+    def get_logger(self):
+        return self.log
+
+    @classmethod
+    def print_msg(cls, msg, level=None, caller=None):
+        sys.stdout.write(cls.color(level))
+        if level:
+            print "%s" % level,
+        if caller:
+            print "[%30s]" % caller,
+        print msg,
+        print cls.color('END')
+
+    #---------------------------------------------------------------------
+    # Log: logger abstraction
+    #---------------------------------------------------------------------
+
+    @classmethod
+    def build_message_string(cls, msg, ctx):
+        if ctx:
+            msg = [m % ctx for m in msg]
+        if isinstance(msg, (tuple, list)):
+            msg = map(lambda s : "%s" % s, msg)
+            msg = " ".join(msg)
+        else:
+            msg = "%s" % msg
+        return msg
+
+    @classmethod
+    def log_message(cls, level, msg, ctx):
+        """
+        \brief Logs an message
+        \param level (string) Log level
+        \param msg (string / list of strings) Message string, or List of message strings
+        \param ctx (dict) Context for the message strings
+        """
+        caller = None
+
+        if not Options().log_duplicates:
+            try:
+                count = cls.seen.get(msg, 0)
+                cls.seen[msg] = count + 1
+            except TypeError, e:
+                # Unhashable types in msg
+                count = 0
+            
+            if count == 1:
+                msg += (" -- REPEATED -- Future similar messages will be silently ignored. Please use the --log_duplicates option to allow for duplicates",)
+            elif count > 1:
+                return
+            
+        if level == 'DEBUG':
+            caller = caller_name(skip=3)
+            # Eventually remove "" added to the configuration file
+            try:
+                paths = tuple(s.strip(' \t\n\r') for s in Options().debug.split(','))
+            except:
+                paths = None
+            if not paths or not caller.startswith(paths):
+                return
+
+        logger = Log().get_logger()
+        msg_str = cls.build_message_string(msg, ctx)
+            
+        if logger:
+            logger_fct = getattr(logger, level.lower())
+            logger_fct("%s(): %s" % (inspect.stack()[2][3], msg_str))
+        else:
+            cls.print_msg(msg_str, level, caller)
+        
+
+    @classmethod
+    def critical(cls, *msg, **ctx):
+        if not Options().log_level in ['CRITICAL']:
+            return
+        cls.log_message('CRITICAL', msg, ctx)
+        sys.exit(0)
+
+    @classmethod
+    def error(cls, *msg, **ctx): 
+        if not Options().log_level in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
+            return
+        cls.log_message('ERROR', msg, ctx)
+        logger = Log().get_logger()
+        if not Log().get_logger():
+            traceback.print_exc()
+        sys.exit(0)
+
+    @classmethod
+    def warning(cls, *msg, **ctx): 
+        if not Options().log_level in ['DEBUG', 'INFO', 'WARNING']:
+            return
+        cls.log_message('WARNING', msg, ctx)
+
+    @classmethod
+    def info(cls, *msg, **ctx):
+        if not Options().log_level in ['DEBUG', 'INFO']:
+            return
+        cls.log_message('INFO', msg, ctx)
+
+    @classmethod
+    def debug(cls, *msg, **ctx):
+        if not Options().log_level in ['DEBUG']:
+            return
+        cls.log_message('DEBUG', msg, ctx)
+
+    @classmethod
+    def tmp(cls, *msg):
+        cls.print_msg(' '.join(map(lambda x: "%r"%x, make_list(msg))), 'TMP', caller_name())
+
+    @classmethod
+    def record(cls, *msg):
+        #cls.print_msg(' '.join(map(lambda x: "%r"%x, make_list(msg))), 'RECORD', caller_name())
+        pass
+
+    @classmethod
+    def deprecated(cls, new):
+        #cls.print_msg("Function %s is deprecated, please use %s" % (caller_name(skip=3), new))
+        pass
+
+Log.init_options()