rewrite msger module based on logging
[tools/mic.git] / mic / msger.py
1 #
2 # Copyright (c) 2013 Intel, Inc.
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License as published by the Free
6 # Software Foundation; version 2 of the License
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc., 59
15 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16
17 """ This logging module is fully compatible with the old msger module, and
18     it supports interactive mode, logs the messages with specified levels
19     to specified stream, can also catch all error messages including the
20     involved 3rd party modules to the logger
21 """
22
23 import os
24 import sys
25 import logging
26 import tempfile
27
28 __ALL__ = [
29     'get_loglevel',
30     'set_loglevel',
31     'set_logfile',
32     'enable_interactive',
33     'disable_interactive',
34     'enable_logstderr',
35     'disable_logstderr',
36     'raw',
37     'debug',
38     'verbose',
39     'info',
40     'warning',
41     'error',
42     'select',
43     'choice',
44     'ask',
45     'pause',
46 ]
47
48
49 # define the color constants
50 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
51
52 # color sequence for tty terminal
53 COLOR_SEQ = "\033[%dm"  # pylint: disable=W1401
54 # reset sequence for tty terminal
55 RESET_SEQ = "\033[0m" # pylint: disable=W1401
56
57 # new log level
58 RAWTEXT = 25
59 VERBOSE = 15
60
61 # define colors for log levels
62 COLORS = {
63     'DEBUG':    COLOR_SEQ % BLUE,
64     'VERBOSE':  COLOR_SEQ % MAGENTA,
65     'INFO':     COLOR_SEQ % GREEN,
66     'WARNING':  COLOR_SEQ % YELLOW,
67     'ERROR':    COLOR_SEQ % RED,
68 }
69
70
71 class LevelFilter(logging.Filter):
72     """ A filter that selects logging message with specified level """
73     def __init__(self, levels):  # pylint: disable=W0231
74         self._levels = levels
75
76     def filter(self, record):
77         if self._levels:
78             return record.levelname in self._levels
79         return False
80
81
82 class MicStreamHandler(logging.StreamHandler):
83     """ A stream handler that print colorized levelname in tty terminal """
84     def __init__(self, stream=None):
85         logging.StreamHandler.__init__(self, stream)
86         msg_fmt = "%(color)s%(levelname)s:%(reset)s %(message)s"
87         self.setFormatter(logging.Formatter(fmt=msg_fmt))
88
89     def _use_color(self):
90         """ Check if to print in color or not """
91         in_emacs = (os.getenv("EMACS") and
92                     os.getenv("INSIDE_EMACS", "").endswith(",comint"))
93         return self.stream.isatty() and not in_emacs
94
95     def format(self, record):
96         """ Format the logging record if need color """
97         record.color = record.reset = ""
98         if self._use_color():
99             record.color = COLORS[record.levelname]
100             record.reset = RESET_SEQ
101         return logging.StreamHandler.format(self, record)
102
103
104 class RedirectedStderr(object):
105     """ A faked error stream that redirect stderr to a temp file """
106     def __init__(self):
107         self.tmpfile = tempfile.NamedTemporaryFile()
108         self.fderr = None
109         self.value = None
110
111     def __del__(self):
112         self.close()
113
114     def close(self):
115         """ Close the temp file and clear the buffer """
116         try:
117             self.value = None
118             self.tmpfile.close()
119         except OSError:
120             pass
121
122     def truncate(self):
123         """ Truncate the tempfile to size zero """
124         if self.tmpfile:
125             os.ftruncate(self.tmpfile.fileno(), 0)
126
127     def redirect(self):
128         """ Redirect stderr to the temp file """
129         self.fderr = os.dup(2)
130         os.dup2(self.tmpfile.fileno(), 2)
131
132     def restore(self):
133         """ Restore the stderr and read the bufferred data """
134         os.dup2(self.fderr, 2)
135         self.fderr = None
136
137         if self.tmpfile:
138             self.tmpfile.seek(0, 0)
139             self.value = self.tmpfile.read()
140
141     def getvalue(self):
142         """ Read the bufferred data """
143         if self.tmpfile:
144             self.tmpfile.seek(0, 0)
145             self.value = self.tmpfile.read()
146             os.ftruncate(self.tmpfile.fileno(), 0)
147             return self.value
148         return None
149
150 class MicFileHandler(logging.FileHandler):
151     """ This file handler is supposed to catch the stderr output from
152         all modules even 3rd party modules involed, as it redirects
153         the stderr stream to a temp file stream, if logfile assigned,
154         it will flush the record to file stream, else it's a buffer
155         handler; once logfile assigned, the buffer will be flushed
156     """
157     def __init__(self, filename=None, mode='w', encoding=None, capacity=10):
158         # we don't use FileHandler to initialize,
159         # because filename might be expected to None
160         logging.Handler.__init__(self)
161         self.stream = None
162         if filename:
163             self.baseFilename = os.path.abspath(filename)
164         else:
165             self.baseFilename = None
166         self.mode = mode
167         self.encoding = None
168         self.capacity = capacity
169         # buffering the records
170         self.buffer = []
171
172         # set formater locally
173         msg_fmt = "[%(asctime)s] %(message)s"
174         date_fmt = "%m/%d %H:%M:%S %Z"
175         self.setFormatter(logging.Formatter(fmt=msg_fmt, datefmt=date_fmt))
176         self.olderr = sys.stderr
177         self.stderr = RedirectedStderr()
178         self.errmsg = None
179
180     def set_logfile(self, filename, mode='w'):
181         """ Set logfile path to make it possible flush records not-on-fly """
182         self.baseFilename = os.path.abspath(filename)
183         self.mode = mode
184
185     def redirect_stderr(self):
186         """ Start to redirect stderr for catching all error output """
187         self.stderr.redirect()
188
189     def restore_stderr(self):
190         """ Restore stderr stream and log the error messages to both stderr
191             and log file if error messages are not empty
192         """
193         self.stderr.restore()
194         self.errmsg = self.stderr.value
195         if self.errmsg:
196             self.logstderr()
197
198     def logstderr(self):
199         """ Log catched error message from stderr redirector """
200         if not self.errmsg:
201             return
202
203         sys.stdout.write(self.errmsg)
204         sys.stdout.flush()
205
206         record = logging.makeLogRecord({'msg': self.errmsg})
207         self.buffer.append(record)
208
209         # truncate the redirector for the errors is logged
210         self.stderr.truncate()
211         self.errmsg = None
212
213     def emit(self, record):
214         """ Emit the log record to Handler """
215         # if there error message catched, log it first
216         self.errmsg = self.stderr.getvalue()
217         if self.errmsg:
218             self.logstderr()
219
220         # if no logfile assigned, it's a buffer handler
221         if not self.baseFilename:
222             self.buffer.append(record)
223             if len(self.buffer) >= self.capacity:
224                 self.buffer = []
225         else:
226             self.flushing(record)
227
228     def flushing(self, record=None):
229         """ Flush buffer and record to logfile """
230         # NOTE: 'flushing' can't be named 'flush' because of 'emit' calling it
231         # set file stream position to SEEK_END(=2)
232         if self.stream:
233             self.stream.seek(0, 2)
234         # if bufferred, flush it
235         if self.buffer:
236             for arecord in self.buffer:
237                 logging.FileHandler.emit(self, arecord)
238             self.buffer = []
239         # if recorded, flush it
240         if record:
241             logging.FileHandler.emit(self, record)
242
243     def close(self):
244         """ Close handler after flushing the buffer """
245         # if any left in buffer, flush it
246         if self.stream:
247             self.flushing()
248         logging.FileHandler.close(self)
249
250
251 class MicLogger(logging.Logger):
252     """ The MIC logger class, it supports interactive mode, and logs the
253         messages with specified levels tospecified stream, also can catch
254         all error messages including the involved 3rd party modules
255     """
256     def __init__(self, name, level=logging.INFO):
257         logging.Logger.__init__(self, name, level)
258         self.interactive = True
259         self.logfile = None
260         self._allhandlers = {
261             'default': logging.StreamHandler(sys.stdout),
262             'stdout': MicStreamHandler(sys.stdout),
263             'stderr': MicStreamHandler(sys.stderr),
264             'logfile': MicFileHandler(),
265         }
266
267         self._allhandlers['default'].addFilter(LevelFilter(['RAWTEXT']))
268         self._allhandlers['default'].setFormatter(
269             logging.Formatter(fmt="%(message)s"))
270         self.addHandler(self._allhandlers['default'])
271
272         self._allhandlers['stdout'].addFilter(LevelFilter(['DEBUG', 'VERBOSE',
273                                                           'INFO']))
274         self.addHandler(self._allhandlers['stdout'])
275
276         self._allhandlers['stderr'].addFilter(LevelFilter(['WARNING',
277                                                            'ERROR']))
278         self.addHandler(self._allhandlers['stderr'])
279
280         self.addHandler(self._allhandlers['logfile'])
281
282     def set_logfile(self, filename, mode='w'):
283         """ Set logfile path """
284         self.logfile = filename
285         self._allhandlers['logfile'].set_logfile(self.logfile, mode)
286
287     def enable_logstderr(self):
288         """ Start to log all error messages """
289         if self.logfile:
290             self._allhandlers['logfile'].redirect_stderr()
291
292     def disable_logstderr(self):
293         """ Stop to log all error messages """
294         if self.logfile:
295             self._allhandlers['logfile'].restore_stderr()
296
297     def verbose(self, msg, *args, **kwargs):
298         """ Log a message with level VERBOSE """
299         if self.isEnabledFor(VERBOSE):
300             self._log(VERBOSE, msg, args, **kwargs)
301
302     def raw(self, msg, *args, **kwargs):
303         """ Log a message in raw text format """
304         if self.isEnabledFor(RAWTEXT):
305             self._log(RAWTEXT, msg, args, **kwargs)
306
307     def select(self, msg, optdict, default=None):
308         """ Log a message in interactive mode """
309         if not optdict.keys():
310             return default
311         if default is None:
312             default = optdict.keys()[0]
313         msg += " [%s](%s): " % ('/'.join(optdict.keys()), default)
314         if not self.interactive or self.logfile:
315             reply = default
316             self.raw(msg + reply)
317         else:
318             while True:
319                 reply = raw_input(msg).strip()
320                 if not reply or reply in optdict:
321                     break
322             if not reply:
323                 reply = default
324         return optdict[reply]
325
326
327 def error(msg):
328     """ Log a message with level ERROR on the MIC logger """
329     LOGGER.error(msg)
330     sys.exit(2)
331
332 def warning(msg):
333     """ Log a message with level WARNING on the MIC logger """
334     LOGGER.warning(msg)
335
336 def info(msg):
337     """ Log a message with level INFO on the MIC logger """
338     LOGGER.info(msg)
339
340 def verbose(msg):
341     """ Log a message with level VERBOSE on the MIC logger """
342     # pylint: disable=E1103
343     LOGGER.verbose(msg)
344
345 def debug(msg):
346     """ Log a message with level DEBUG on the MIC logger """
347     LOGGER.debug(msg)
348
349 def raw(msg):
350     """ Log a message on the MIC logger in raw text format"""
351     # pylint: disable=E1103
352     LOGGER.raw(msg)
353
354 def select(msg, optdict, default=None):
355     """ Show an interactive scene in tty terminal and
356         logs them on MIC logger
357     """
358     # pylint: disable=E1103
359     return LOGGER.select(msg, optdict, default)
360
361 def choice(msg, optlist, default=0):
362     """ Give some alternatives to users for answering the question """
363     # pylint: disable=E1103
364     return LOGGER.select(msg, dict(zip(optlist, optlist)), optlist[default])
365
366 def ask(msg, ret=True):
367     """ Ask users to answer 'yes' or 'no' to the question """
368     answers = {'y': True, 'n': False}
369     default = {True: 'y', False: 'n'}[ret]
370     # pylint: disable=E1103
371     return LOGGER.select(msg, answers, default)
372
373 def pause(msg=None):
374     """ Pause for any key """
375     if msg is None:
376         msg = "press ANY KEY to continue ..."
377     raw_input(msg)
378
379 def set_logfile(logfile, mode='w'):
380     """ Set logfile path to the MIC logger """
381     # pylint: disable=E1103
382     LOGGER.set_logfile(logfile, mode)
383
384 def set_loglevel(level):
385     """ Set loglevel to the MIC logger """
386     if isinstance(level, basestring):
387         level = logging.getLevelName(level)
388     LOGGER.setLevel(level)
389
390 def get_loglevel():
391     """ Get the loglevel of the MIC logger """
392     return logging.getLevelName(LOGGER.level)
393
394 def disable_interactive():
395     """ Disable the interactive mode """
396     LOGGER.interactive = False
397
398 def enable_interactive():
399     """ Enable the interactive mode """
400     LOGGER.interactive = True
401
402 def set_interactive(value):
403     """ Set the interactive mode (for compatibility) """
404     if value:
405         enable_interactive()
406     else:
407         disable_interactive()
408
409 def enable_logstderr(fpath=None):  # pylint: disable=W0613
410     """ Start to log all error message on the MIC logger """
411     # pylint: disable=E1103
412     LOGGER.enable_logstderr()
413
414 def disable_logstderr():
415     """ Stop to log all error message on the MIC logger """
416     # pylint: disable=E1103
417     LOGGER.disable_logstderr()
418
419
420 # add two level to the MIC logger: 'VERBOSE', 'RAWTEXT'
421 logging.addLevelName(VERBOSE, 'VERBOSE')
422 logging.addLevelName(RAWTEXT, 'RAWTEXT')
423 # initial the MIC logger
424 logging.setLoggerClass(MicLogger)
425 LOGGER = logging.getLogger("MIC")