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