Autocomplete working in query_editor plugin
[myslice.git] / manifold / util / log.py
1 import sys, logging, traceback, inspect, os.path
2 from logging                 import handlers
3 from manifold.util.singleton import Singleton
4 from manifold.util.options   import Options
5 from manifold.util.misc      import caller_name, make_list
6 from manifold.util           import colors
7
8 # TODO Log should take separately message strings and arguments to be able to
9 # remember which messages are seen several times, and also to allow for
10 # translation
11 # TODO How to log to stdout without putting None in self.log
12
13 class Log(object):
14     __metaclass__ = Singleton
15
16     DEFAULTS = {
17         # Logging
18         "rsyslog_enable"      : False,
19         "rsyslog_host"        : None, #"log.top-hat.info",
20         "rsyslog_port"        : None, #28514,
21         "log_file"            : "/var/log/manifold.log",
22         "log_level"           : "DEBUG",
23         "debug"               : "default",
24         "log_duplicates"      : False
25     }
26
27     # COLORS
28     color_ansi = {
29         'DEBUG'  : colors.MYGREEN,
30         'INFO'   : colors.MYBLUE,
31         'WARNING': colors.MYWARNING,
32         'ERROR'  : colors.MYRED,
33         'HEADER' : colors.MYHEADER,
34         'END'    : colors.MYEND,
35         'RECORD' : colors.MYBLUE,
36         'TMP'    : colors.MYRED,
37     }
38
39     @classmethod
40     def color(cls, color):
41         return cls.color_ansi[color] if color else ''
42
43     # To remove duplicate messages
44     seen = {}
45
46     def __init__(self, name='(default)'):
47         self.log = None # logging.getLogger(name)
48         self.files_to_keep = []
49         self.init_log()
50         self.color = True
51
52
53     @classmethod
54     def init_options(self):
55         opt = Options()
56
57         opt.add_option(
58             "--rsyslog-enable", action = "store_false", dest = "rsyslog_enable",
59             help = "Specify if log have to be written to a rsyslog server.",
60             default = self.DEFAULTS["rsyslog_enable"]
61         )
62         opt.add_option(
63             "--rsyslog-host", dest = "rsyslog_host",
64             help = "Rsyslog hostname.",
65             default = self.DEFAULTS["rsyslog_host"]
66         )
67         opt.add_option(
68             "--rsyslog-port", type = "int", dest = "rsyslog_port",
69             help = "Rsyslog port.",
70             default = self.DEFAULTS["rsyslog_port"]
71         )
72         opt.add_option(
73             "-o", "--log-file", dest = "log_file",
74             help = "Log filename.",
75             default = self.DEFAULTS["log_file"]
76         )
77         opt.add_option(
78             "-L", "--log-level", dest = "log_level",
79             choices = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
80             help = "Log level",
81             default = self.DEFAULTS["log_level"]
82         )
83         opt.add_option(
84             "-d", "--debug", dest = "debug",
85             help = "Debug paths (a list of coma-separated python path: path.to.module.function).",
86             default = self.DEFAULTS["debug"]
87         )
88         opt.add_option(
89             "", "--log_duplicates", action = "store_true", dest = "log_duplicates",
90             help = "Remove duplicate messages in logs",
91             default = self.DEFAULTS["log_duplicates"]
92         )
93
94     def init_log(self, options=object()):
95         # Initialize self.log (require self.files_to_keep)
96         if self.log: # for debugging by using stdout, log may be equal to None
97             if Options().rsyslog_host:
98                 shandler = self.make_handler_rsyslog(
99                     Options().rsyslog_host,
100                     Options().rsyslog_port,
101                     Options().log_level
102                 )
103             elif Options().log_file:
104                 shandler = self.make_handler_locallog(
105                     Options().log_file,
106                     Options().log_level
107                 )
108
109     #------------------------------------------------------------------------
110     # Log
111     #------------------------------------------------------------------------
112
113     def make_handler_rsyslog(self, rsyslog_host, rsyslog_port, log_level):
114         """
115         \brief (Internal usage) Prepare logging via rsyslog
116         \param rsyslog_host The hostname of the rsyslog server
117         \param rsyslog_port The port of the rsyslog server
118         \param log_level Log level
119         """
120         # Prepare the handler
121         shandler = handlers.SysLogHandler(
122             (rsyslog_host, rsyslog_port),
123             facility = handlers.SysLogHandler.LOG_DAEMON
124         )
125
126         # The log file must remain open while daemonizing 
127         self.prepare_handler(shandler, log_level)
128         return shandler
129
130     def make_handler_locallog(self, log_filename, log_level):
131         """
132         \brief (Internal usage) Prepare local logging
133         \param log_filename The file in which we write the logs
134         \param log_level Log level
135         """
136         # Create directory in which we store the log file
137         log_dir = os.path.dirname(log_filename)
138         if log_dir and not os.path.exists(log_dir):
139             try:
140                 os.makedirs(log_dir)
141             except OSError, why:
142                 # XXX here we don't log since log is not initialized yet
143                 print "OS error: %s" % why
144
145         # Prepare the handler
146         shandler = logging.handlers.RotatingFileHandler(
147             log_filename,
148             backupCount = 0
149         )
150
151         # The log file must remain open while daemonizing 
152         self.files_to_keep.append(shandler.stream)
153         self.prepare_handler(shandler, log_level)
154         return shandler
155
156     def prepare_handler(self, shandler, log_level):
157         """
158         \brief (Internal usage)
159         \param shandler Handler used to log information
160         \param log_level Log level
161         """
162         shandler.setLevel(log_level)
163         formatter = logging.Formatter("%(asctime)s: %(name)s: %(levelname)s %(message)s")
164         shandler.setFormatter(formatter)
165         self.log.addHandler(shandler)
166         self.log.setLevel(getattr(logging, log_level, logging.INFO))
167                       
168     def get_logger(self):
169         return self.log
170
171     @classmethod
172     def print_msg(cls, msg, level=None, caller=None):
173         sys.stdout.write(cls.color(level))
174         if level:
175             print "%s" % level,
176         if caller:
177             print "[%30s]" % caller,
178         print msg,
179         print cls.color('END')
180
181     #---------------------------------------------------------------------
182     # Log: logger abstraction
183     #---------------------------------------------------------------------
184
185     @classmethod
186     def build_message_string(cls, msg, ctx):
187         if ctx:
188             msg = [m % ctx for m in msg]
189         if isinstance(msg, (tuple, list)):
190             msg = map(lambda s : "%s" % s, msg)
191             msg = " ".join(msg)
192         else:
193             msg = "%s" % msg
194         return msg
195
196     @classmethod
197     def log_message(cls, level, msg, ctx):
198         """
199         \brief Logs an message
200         \param level (string) Log level
201         \param msg (string / list of strings) Message string, or List of message strings
202         \param ctx (dict) Context for the message strings
203         """
204         caller = None
205
206         if not Options().log_duplicates:
207             try:
208                 count = cls.seen.get(msg, 0)
209                 cls.seen[msg] = count + 1
210             except TypeError, e:
211                 # Unhashable types in msg
212                 count = 0
213             
214             if count == 1:
215                 msg += (" -- REPEATED -- Future similar messages will be silently ignored. Please use the --log_duplicates option to allow for duplicates",)
216             elif count > 1:
217                 return
218             
219         if level == 'DEBUG':
220             caller = caller_name(skip=3)
221             # Eventually remove "" added to the configuration file
222             try:
223                 paths = tuple(s.strip(' \t\n\r') for s in Options().debug.split(','))
224             except:
225                 paths = None
226             if not paths or not caller.startswith(paths):
227                 return
228
229         logger = Log().get_logger()
230         msg_str = cls.build_message_string(msg, ctx)
231             
232         if logger:
233             logger_fct = getattr(logger, level.lower())
234             logger_fct("%s(): %s" % (inspect.stack()[2][3], msg_str))
235         else:
236             cls.print_msg(msg_str, level, caller)
237         
238
239     @classmethod
240     def critical(cls, *msg, **ctx):
241         if not Options().log_level in ['CRITICAL']:
242             return
243         cls.log_message('CRITICAL', msg, ctx)
244         sys.exit(0)
245
246     @classmethod
247     def error(cls, *msg, **ctx): 
248         if not Options().log_level in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
249             return
250         cls.log_message('ERROR', msg, ctx)
251         logger = Log().get_logger()
252         if not Log().get_logger():
253             traceback.print_exc()
254         sys.exit(0)
255
256     @classmethod
257     def warning(cls, *msg, **ctx): 
258         if not Options().log_level in ['DEBUG', 'INFO', 'WARNING']:
259             return
260         cls.log_message('WARNING', msg, ctx)
261
262     @classmethod
263     def info(cls, *msg, **ctx):
264         if not Options().log_level in ['DEBUG', 'INFO']:
265             return
266         cls.log_message('INFO', msg, ctx)
267
268     @classmethod
269     def debug(cls, *msg, **ctx):
270         if not Options().log_level in ['DEBUG']:
271             return
272         cls.log_message('DEBUG', msg, ctx)
273
274     @classmethod
275     def tmp(cls, *msg):
276         cls.print_msg(' '.join(map(lambda x: "%r"%x, make_list(msg))), 'TMP', caller_name())
277
278     @classmethod
279     def record(cls, *msg):
280         #cls.print_msg(' '.join(map(lambda x: "%r"%x, make_list(msg))), 'RECORD', caller_name())
281         pass
282
283     @classmethod
284     def deprecated(cls, new):
285         #cls.print_msg("Function %s is deprecated, please use %s" % (caller_name(skip=3), new))
286         pass
287
288 Log.init_options()