2 # Copyright (c) 2013 Intel, Inc.
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
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
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.
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
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
43 'disable_interactive',
59 # define the color constants
60 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = list(range(30, 38))
62 # color sequence for tty terminal
63 COLOR_SEQ = "\033[%dm"
64 # reset sequence for tty terminal
71 # define colors for log levels
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,
81 class LevelFilter(logging.Filter):
82 """ A filter that selects logging message with specified level """
83 def __init__(self, levels):
86 def filter(self, record):
88 return record.levelname in self._levels
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))
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
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)
114 class RedirectedStderr(object):
115 """ A faked error stream that redirect stderr to a temp file """
117 self.tmpfile = tempfile.NamedTemporaryFile()
125 """ Close the temp file and clear the buffer """
133 """ Truncate the tempfile to size zero """
135 os.ftruncate(self.tmpfile.fileno(), 0)
136 os.lseek(self.tmpfile.fileno(), 0, os.SEEK_SET)
139 """ Redirect stderr to the temp file """
140 self.fderr = os.dup(2)
141 os.dup2(self.tmpfile.fileno(), 2)
144 """ Restore the stderr and read the bufferred data """
145 os.dup2(self.fderr, 2)
149 self.tmpfile.seek(0, 0)
150 self.value = self.tmpfile.read()
153 """ Read the bufferred data """
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)
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
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._builtin_open = open
177 self.baseFilename = os.path.abspath(filename)
179 self.baseFilename = None
182 self.capacity = capacity
183 # buffering the records
186 # set formater locally
187 msg_fmt = "[%(asctime)s] %(message)s"
188 date_fmt = "%m/%d %H:%M:%S %Z"
189 self.setFormatter(logging.Formatter(fmt=msg_fmt, datefmt=date_fmt))
190 self.olderr = sys.stderr
191 self.stderr = RedirectedStderr()
194 def set_logfile(self, filename, mode='w'):
195 """ Set logfile path to make it possible flush records not-on-fly """
196 self.baseFilename = os.path.abspath(filename)
199 def redirect_stderr(self):
200 """ Start to redirect stderr for catching all error output """
201 self.stderr.redirect()
203 def restore_stderr(self):
204 """ Restore stderr stream and log the error messages to both stderr
205 and log file if error messages are not empty
207 self.stderr.restore()
208 self.errmsg = self.stderr.value
213 """ Log catched error message from stderr redirector """
217 sys.stdout.write(self.errmsg)
220 record = logging.makeLogRecord({'msg': self.errmsg})
221 self.buffer.append(record)
223 # truncate the redirector for the errors is logged
224 self.stderr.truncate()
227 def emit(self, record):
228 """ Emit the log record to Handler """
229 # if there error message catched, log it first
230 self.errmsg = self.stderr.getvalue()
234 # if no logfile assigned, it's a buffer handler
235 if not self.baseFilename:
236 self.buffer.append(record)
237 if len(self.buffer) >= self.capacity:
240 self.flushing(record)
242 def flushing(self, record=None):
243 """ Flush buffer and record to logfile """
244 # NOTE: 'flushing' can't be named 'flush' because of 'emit' calling it
245 # set file stream position to SEEK_END(=2)
247 self.stream.seek(0, 2)
248 # if bufferred, flush it
250 for arecord in self.buffer:
251 logging.FileHandler.emit(self, arecord)
253 # if recorded, flush it
255 logging.FileHandler.emit(self, record)
258 """ Close handler after flushing the buffer """
259 # if any left in buffer, flush it
262 logging.FileHandler.close(self)
265 class MicLogger(logging.Logger):
266 """ The MIC logger class, it supports interactive mode, and logs the
267 messages with specified levels tospecified stream, also can catch
268 all error messages including the involved 3rd party modules
270 def __init__(self, name, level=logging.INFO):
271 logging.Logger.__init__(self, name, level)
272 self.propagate = False
273 self.interactive = True
275 self._allhandlers = {
276 'default': logging.StreamHandler(sys.stdout),
277 'stdout': MicStreamHandler(sys.stdout),
278 'stderr': MicStreamHandler(sys.stderr),
279 'logfile': MicFileHandler(),
282 self._allhandlers['default'].addFilter(LevelFilter(['RAWTEXT']))
283 self._allhandlers['default'].setFormatter(
284 logging.Formatter(fmt="%(message)s"))
285 self.addHandler(self._allhandlers['default'])
287 self._allhandlers['stdout'].addFilter(LevelFilter(['DEBUG', 'VERBOSE',
289 self.addHandler(self._allhandlers['stdout'])
291 self._allhandlers['stderr'].addFilter(LevelFilter(['WARNING',
293 self.addHandler(self._allhandlers['stderr'])
295 self.addHandler(self._allhandlers['logfile'])
297 def set_logfile(self, filename, mode='w'):
298 """ Set logfile path """
299 self.logfile = filename
300 self._allhandlers['logfile'].set_logfile(self.logfile, mode)
302 def enable_logstderr(self):
303 """ Start to log all error messages """
305 self._allhandlers['logfile'].redirect_stderr()
307 def disable_logstderr(self):
308 """ Stop to log all error messages """
310 self._allhandlers['logfile'].restore_stderr()
312 def verbose(self, msg, *args, **kwargs):
313 """ Log a message with level VERBOSE """
314 if self.isEnabledFor(VERBOSE):
315 self._log(VERBOSE, msg, args, **kwargs)
317 def raw(self, msg, *args, **kwargs):
318 """ Log a message in raw text format """
319 if self.isEnabledFor(RAWTEXT):
320 self._log(RAWTEXT, msg, args, **kwargs)
322 def select(self, msg, optdict, default=None):
323 """ Log a message in interactive mode """
324 if not list(optdict.keys()):
327 default = list(optdict.keys())[0]
328 msg += " [%s](%s): " % ('/'.join(list(optdict.keys())), default)
329 if not self.interactive or self.logfile:
331 self.raw(msg + reply)
334 reply = input(msg).strip()
335 if not reply or reply in optdict:
339 return optdict[reply]
343 """ Log a message with level ERROR on the MIC logger """
348 """ Log a message with level WARNING on the MIC logger """
352 """ Log a message with level INFO on the MIC logger """
356 """ Log a message with level VERBOSE on the MIC logger """
360 """ Log a message with level DEBUG on the MIC logger """
364 """ Log a message on the MIC logger in raw text format"""
367 def select(msg, optdict, default=None):
368 """ Show an interactive scene in tty terminal and
369 logs them on MIC logger
371 return LOGGER.select(msg, optdict, default)
373 def choice(msg, optlist, default=0):
374 """ Give some alternatives to users for answering the question """
375 return LOGGER.select(msg, dict(list(zip(optlist, optlist))), optlist[default])
377 def ask(msg, ret=True):
378 """ Ask users to answer 'yes' or 'no' to the question """
379 answers = {'y': True, 'n': False}
380 default = {True: 'y', False: 'n'}[ret]
381 return LOGGER.select(msg, answers, default)
384 """ Pause for any key """
386 msg = "press ANY KEY to continue ..."
389 def set_logfile(logfile, mode='w'):
390 """ Set logfile path to the MIC logger """
391 LOGGER.set_logfile(logfile, mode)
393 def set_loglevel(level):
394 """ Set loglevel to the MIC logger """
395 if isinstance(level, str):
396 level = logging.getLevelName(level)
397 LOGGER.setLevel(level)
400 """ Get the loglevel of the MIC logger """
401 return logging.getLevelName(LOGGER.level)
403 def disable_interactive():
404 """ Disable the interactive mode """
405 LOGGER.interactive = False
407 def enable_interactive():
408 """ Enable the interactive mode """
409 LOGGER.interactive = True
411 def set_interactive(value):
412 """ Set the interactive mode (for compatibility) """
416 disable_interactive()
418 def enable_logstderr(fpath=None):
419 """ Start to log all error message on the MIC logger """
420 LOGGER.enable_logstderr()
422 def disable_logstderr():
423 """ Stop to log all error message on the MIC logger """
424 LOGGER.disable_logstderr()
427 # add two level to the MIC logger: 'VERBOSE', 'RAWTEXT'
428 logging.addLevelName(VERBOSE, 'VERBOSE')
429 logging.addLevelName(RAWTEXT, 'RAWTEXT')
430 # initial the MIC logger
431 logging.setLoggerClass(MicLogger)
432 LOGGER = logging.getLogger("MIC")