--- /dev/null
+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()