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()
151 if isinstance(self.value, bytes):
152 self.value = self.value.decode()
155 """ Read the bufferred data """
157 self.tmpfile.seek(0, 0)
158 self.value = self.tmpfile.read()
159 os.ftruncate(self.tmpfile.fileno(), 0)
160 os.lseek(self.tmpfile.fileno(), 0, os.SEEK_SET)
161 if isinstance(self.value, bytes):
162 self.value = self.value.decode()
166 class MicFileHandler(logging.FileHandler):
167 """ This file handler is supposed to catch the stderr output from
168 all modules even 3rd party modules involed, as it redirects
169 the stderr stream to a temp file stream, if logfile assigned,
170 it will flush the record to file stream, else it's a buffer
171 handler; once logfile assigned, the buffer will be flushed
173 def __init__(self, filename=None, mode='w', encoding=None, capacity=10):
174 # we don't use FileHandler to initialize,
175 # because filename might be expected to None
176 logging.Handler.__init__(self)
177 self._builtin_open = open
181 self.baseFilename = os.path.abspath(filename)
183 self.baseFilename = None
186 self.capacity = capacity
187 # buffering the records
190 # set formater locally
191 msg_fmt = "[%(asctime)s] %(message)s"
192 date_fmt = "%m/%d %H:%M:%S %Z"
193 self.setFormatter(logging.Formatter(fmt=msg_fmt, datefmt=date_fmt))
194 self.olderr = sys.stderr
195 self.stderr = RedirectedStderr()
198 def set_logfile(self, filename, mode='w'):
199 """ Set logfile path to make it possible flush records not-on-fly """
200 self.baseFilename = os.path.abspath(filename)
203 def redirect_stderr(self):
204 """ Start to redirect stderr for catching all error output """
205 self.stderr.redirect()
207 def restore_stderr(self):
208 """ Restore stderr stream and log the error messages to both stderr
209 and log file if error messages are not empty
211 self.stderr.restore()
212 self.errmsg = self.stderr.value
217 """ Log catched error message from stderr redirector """
221 if isinstance(self.errmsg, bytes):
222 self.errmsg = self.errmsg.decode()
223 sys.stdout.write(self.errmsg)
226 record = logging.makeLogRecord({'msg': self.errmsg})
227 self.buffer.append(record)
229 # truncate the redirector for the errors is logged
230 self.stderr.truncate()
233 def emit(self, record):
234 """ Emit the log record to Handler """
235 # if there error message catched, log it first
236 self.errmsg = self.stderr.getvalue()
240 # if no logfile assigned, it's a buffer handler
241 if not self.baseFilename:
242 self.buffer.append(record)
243 if len(self.buffer) >= self.capacity:
246 self.flushing(record)
248 def flushing(self, record=None):
249 """ Flush buffer and record to logfile """
250 # NOTE: 'flushing' can't be named 'flush' because of 'emit' calling it
251 # set file stream position to SEEK_END(=2)
253 self.stream.seek(0, 2)
254 # if bufferred, flush it
256 for arecord in self.buffer:
257 logging.FileHandler.emit(self, arecord)
259 # if recorded, flush it
261 logging.FileHandler.emit(self, record)
264 """ Close handler after flushing the buffer """
265 # if any left in buffer, flush it
268 logging.FileHandler.close(self)
271 class MicLogger(logging.Logger):
272 """ The MIC logger class, it supports interactive mode, and logs the
273 messages with specified levels tospecified stream, also can catch
274 all error messages including the involved 3rd party modules
276 def __init__(self, name, level=logging.INFO):
277 logging.Logger.__init__(self, name, level)
278 self.propagate = False
279 self.interactive = True
281 self._allhandlers = {
282 'default': logging.StreamHandler(sys.stdout),
283 'stdout': MicStreamHandler(sys.stdout),
284 'stderr': MicStreamHandler(sys.stderr),
285 'logfile': MicFileHandler(),
288 self._allhandlers['default'].addFilter(LevelFilter(['RAWTEXT']))
289 self._allhandlers['default'].setFormatter(
290 logging.Formatter(fmt="%(message)s"))
291 self.addHandler(self._allhandlers['default'])
293 self._allhandlers['stdout'].addFilter(LevelFilter(['DEBUG', 'VERBOSE',
295 self.addHandler(self._allhandlers['stdout'])
297 self._allhandlers['stderr'].addFilter(LevelFilter(['WARNING',
299 self.addHandler(self._allhandlers['stderr'])
301 self.addHandler(self._allhandlers['logfile'])
303 def set_logfile(self, filename, mode='w'):
304 """ Set logfile path """
305 self.logfile = filename
306 self._allhandlers['logfile'].set_logfile(self.logfile, mode)
308 def enable_logstderr(self):
309 """ Start to log all error messages """
311 self._allhandlers['logfile'].redirect_stderr()
313 def disable_logstderr(self):
314 """ Stop to log all error messages """
316 self._allhandlers['logfile'].restore_stderr()
318 def verbose(self, msg, *args, **kwargs):
319 """ Log a message with level VERBOSE """
320 if self.isEnabledFor(VERBOSE):
321 self._log(VERBOSE, msg, args, **kwargs)
323 def raw(self, msg, *args, **kwargs):
324 """ Log a message in raw text format """
325 if self.isEnabledFor(RAWTEXT):
326 self._log(RAWTEXT, msg, args, **kwargs)
328 def select(self, msg, optdict, default=None):
329 """ Log a message in interactive mode """
330 if not list(optdict.keys()):
333 default = list(optdict.keys())[0]
334 msg += " [%s](%s): " % ('/'.join(list(optdict.keys())), default)
335 if not self.interactive or self.logfile:
337 self.raw(msg + reply)
340 reply = input(msg).strip()
341 if not reply or reply in optdict:
345 return optdict[reply]
349 """ Log a message with level ERROR on the MIC logger """
354 """ Log a message with level WARNING on the MIC logger """
358 """ Log a message with level INFO on the MIC logger """
362 """ Log a message with level VERBOSE on the MIC logger """
366 """ Log a message with level DEBUG on the MIC logger """
370 """ Log a message on the MIC logger in raw text format"""
373 def select(msg, optdict, default=None):
374 """ Show an interactive scene in tty terminal and
375 logs them on MIC logger
377 return LOGGER.select(msg, optdict, default)
379 def choice(msg, optlist, default=0):
380 """ Give some alternatives to users for answering the question """
381 return LOGGER.select(msg, dict(list(zip(optlist, optlist))), optlist[default])
383 def ask(msg, ret=True):
384 """ Ask users to answer 'yes' or 'no' to the question """
385 answers = {'y': True, 'n': False}
386 default = {True: 'y', False: 'n'}[ret]
387 return LOGGER.select(msg, answers, default)
390 """ Pause for any key """
392 msg = "press ANY KEY to continue ..."
395 def set_logfile(logfile, mode='w'):
396 """ Set logfile path to the MIC logger """
397 LOGGER.set_logfile(logfile, mode)
399 def set_loglevel(level):
400 """ Set loglevel to the MIC logger """
401 if isinstance(level, str):
402 level = logging.getLevelName(level)
403 LOGGER.setLevel(level)
406 """ Get the loglevel of the MIC logger """
407 return logging.getLevelName(LOGGER.level)
409 def disable_interactive():
410 """ Disable the interactive mode """
411 LOGGER.interactive = False
413 def enable_interactive():
414 """ Enable the interactive mode """
415 LOGGER.interactive = True
417 def set_interactive(value):
418 """ Set the interactive mode (for compatibility) """
422 disable_interactive()
424 def enable_logstderr(fpath=None):
425 """ Start to log all error message on the MIC logger """
426 LOGGER.enable_logstderr()
428 def disable_logstderr():
429 """ Stop to log all error message on the MIC logger """
430 LOGGER.disable_logstderr()
433 # add two level to the MIC logger: 'VERBOSE', 'RAWTEXT'
434 logging.addLevelName(VERBOSE, 'VERBOSE')
435 logging.addLevelName(RAWTEXT, 'RAWTEXT')
436 # initial the MIC logger
437 logging.setLoggerClass(MicLogger)
438 LOGGER = logging.getLogger("MIC")