From: Christian Van Wambeke Date: Tue, 15 May 2018 14:49:11 +0000 (+0200) Subject: begin logging xml files X-Git-Url: http://git.salome-platform.org/gitweb/?a=commitdiff_plain;h=30abae5ea236f80b46ec8b93ba03fb21110c2f81;p=tools%2Fsat.git begin logging xml files --- diff --git a/data/local.pyconf b/data/local.pyconf index 7e6644a..ed70f4b 100644 --- a/data/local.pyconf +++ b/data/local.pyconf @@ -13,12 +13,6 @@ project_file_paths : [ "/volatile/wambeke/SAT5/SAT5_S840_MATIX24/SAT_SALOME/salome.pyconf", - # "/home/uranietm/proJET/saTJOBS/saT5/uranie.pyconf", - # cloned 2017/12 for matix - ##"/home/matix/GitRepo/uranie/saT5/uranie.pyconf", "/volatile/wambeke/SAT5/SAT_MATIX/matix.pyconf" - "/home/christian/SAT_SALOME/salome.pyconf" - "/home/christian/SAT_MATIX/matix.pyconf" - #"/home/christian/SAT_MATIX" ] } diff --git a/data/local_original.pyconf b/data/local_original.pyconf new file mode 100644 index 0000000..7755af0 --- /dev/null +++ b/data/local_original.pyconf @@ -0,0 +1,16 @@ + + LOCAL : + { + base : 'default' + workdir : 'default' + log_dir : 'default' + archive_dir : 'default' + VCS : None + tag : None + } + PROJECTS : + { + project_file_paths : + [ + ] + } diff --git a/data/local_wambeke_cea.pyconf b/data/local_wambeke_cea.pyconf new file mode 100644 index 0000000..e62c609 --- /dev/null +++ b/data/local_wambeke_cea.pyconf @@ -0,0 +1,21 @@ + + LOCAL : + { + base : 'default' + workdir : 'default' + log_dir : 'default' + archive_dir : 'default' + VCS : None + tag : None + } + PROJECTS : + { + project_file_paths : + [ + "/volatile/wambeke/SAT5/SAT5_S840_MATIX24/SAT_SALOME/salome.pyconf", + # "/home/uranietm/proJET/saTJOBS/saT5/uranie.pyconf", + # cloned 2017/12 for matix + # "/home/matix/GitRepo/uranie/saT5/uranie.pyconf", + # "/volatile/wambeke/SAT5/SAT_MATIX/matix.pyconf" + ] + } diff --git a/data/local_wambeke_home.pyconf b/data/local_wambeke_home.pyconf new file mode 100644 index 0000000..c91da53 --- /dev/null +++ b/data/local_wambeke_home.pyconf @@ -0,0 +1,18 @@ + + LOCAL : + { + base : 'default' + workdir : 'default' + log_dir : 'default' + archive_dir : 'default' + VCS : None + tag : None + } + PROJECTS : + { + project_file_paths : + [ + "/home/christian/SAT_SALOME/salome.pyconf" + "/home/christian/SAT_MATIX/matix.pyconf" + ] + } diff --git a/sat b/sat index 9ac9b8f..f36b1cb 100755 --- a/sat +++ b/sat @@ -60,17 +60,20 @@ if __name__ == "__main__": else: # warning as known problem logger.warning("sat exit code: %s" % returnCode) # KO have to say why + logger.close() sys.exit(returnCode.toSys()) except Exception as e: # error as may be unknown problem # verbose debug message with traceback if developers msg = "Exception raised for execute_cli(%s):\n" % args - logger.critical(DBG.format_color_exception(msg)) + logger.critical(DBG.format_color_exception(msg)) + logger.close() sys.exit(KOSYS) else: logger.critical("forbidden/unexpected mode for __name__ '%s'" % __name__) + logger.close() sys.exit(KOSYS) diff --git a/src/loggingSat.py b/src/loggingSat.py index 29e7e94..9286bad 100755 --- a/src/loggingSat.py +++ b/src/loggingSat.py @@ -23,10 +23,27 @@ salomeTools logger. using logging package | formatted indented on multi lines messages using handlers """ +""" +to launch example: + +>> export TRG=SALOME-8.4.0 +>> cd .../sat5.1 +>> src/loggingSat.py +>> AllTestLauncherSat.py -p 'test_???_logging*.py' +>> export TRG=SALOME-8.4.0 +>> sat config $TRG -i KERNEL +>> sat config -v LOCAL.log_dir +>> rm -rf /volatile/wambeke/SAT5/SAT5_S840_MATIX24/LOGS +>> sat prepare $TRG -p KERNEL +>> sat log +""" + import os import sys import logging as LOGI +from logging.handlers import BufferingHandler import pprint as PP +import src.utilsSat as UTS import src.coloringSat as COLS _verbose = False @@ -34,7 +51,12 @@ _name = "loggingSat" _loggerDefaultName = 'SatDefaultLogger' _loggerUnittestName = 'SatUnittestLogger' +_TRACE = LOGI.INFO - 2 # trace level is just below INFO +LOGI.TRACE = _TRACE # only for coherency, +################################################################# +# utilities methods +################################################################# def indent(msg, nb, car=" "): """indent nb car (spaces) multi lines message except first one""" s = msg.split("\n") @@ -51,23 +73,68 @@ def indentUnittest(msg, prefix=" | "): res = ("\n" + prefix).join(s) return res -def log(msg): +def log(msg, force=False): """elementary log when no logging.Logger yet""" prefix = "%s.log: " % _name nb = len(prefix) - if _verbose: print(prefix + indent(msg, nb)) + if _verbose or force: + print(prefix + indent(msg, nb)) log("import logging on %s" % LOGI.__file__) -def dirLogger(logger): - logger.info('dir(logger name=%s):\n' % logger.name, PP.pformat(dir(logger))) +def getStrDirLogger(logger): + """ + Returns multi line string for logger description, with dir(logger). + Used for debug + """ + lgr = logger # shortcut + msg = "%s(name=%s, dateLogger=%s):\n%s\n" + cName = lgr.__class__.__name__ + res = msg % (cName, lgr.name, lgr.dateLogger, PP.pformat(dir(lgr))) + return res +def getStrHandler(handler): + """ + Returns one line string for handler description + (as inexisting __repr__) + to avoid create inherited classe(s) handler + """ + h = handler # shortcut + msg = "%s(name=%s)" + cName = h.__class__.__name__ + res = msg % (cName, h.get_name()) + return res + +def getStrShort(msg): + """Returns short string for msg (as first caracters without line feed""" + res = msg.replace("\n", "//")[0:30] + return res + +def getStrLogRecord(logRecord): + """ + Returns one line string for simple logging LogRecord description + """ + msg = "LogRecord(level='%s', msg='%s...')" + shortMsg = getStrShort(logRecord.msg) + levelName = COLS.cleanColors(logRecord.levelname).replace(" ", "") + res = msg % (levelName, shortMsg) + return res +def getListOfStrLogRecord(listOfLogRecord): + """ + Returns one line string for logging LogRecord description + """ + res = [getStrLogRecord(l) for l in listOfLogRecord] + return res + +################################################################# +# salometools logger classes +################################################################# class LoggerSat(LOGI.Logger): """ - inherited class logging.Logger for logger salomeTools + Inherited class logging.Logger for logger salomeTools | add a level TRACE as log.trace(msg) | below log.info(msg) @@ -77,38 +144,64 @@ class LoggerSat(LOGI.Logger): | see: /usr/lib64/python2.7/logging/__init__.py etc. """ - _TRACE = LOGI.INFO - 2 # just below - def __init__(self, name, level=LOGI.INFO): """ Initialize the logger with a name and an optional level. """ super(LoggerSat, self).__init__(name, level) - LOGI.addLevelName(self._TRACE, "TRACE") - # LOGI.TRACE = self._TRACE # only for coherency, + LOGI.addLevelName(_TRACE, "TRACE") + self.dateLogger = "NoDateLogger" + self.closed = False + + def close(self): + """ + final stuff for logger, done at end salomeTools + flushed and closed xml files have to be not overriden/appended + """ + if self.closed: + raise Exception("logger closed yet: %s" % self) + log("close stuff logger %s" % self, True) # getStrDirLogger(self) + for handl in self.handlers: + log("close stuff handler %s" % getStrHandler(handl), True) + handl.close() # Tidy up any resources used by the handler. + # todo etc + self.closed = True # done at end sat, flushed closed xml files. + return + + def __repr__(self): + """one line string representation""" + msg = "%s(name=%s, dateLogger=%s, handlers=%s)" + cName = self.__class__.__name__ + h = [getStrHandler(h) for h in self.handlers] + h = "[" + ", ".join(h) + "]" + res = msg % (cName, self.name, self.dateLogger, h) + return res def trace(self, msg, *args, **kwargs): """ Log 'msg % args' with severity '_TRACE'. - To pass exception information, use the keyword argument exc_info with - a true value, e.g. - - logger.trace("Houston, we have a %s", "long trace to follow") + | To pass exception information, + | use the keyword argument exc_info with a true value + | >> logger.trace("Houston, we have a %s", "long trace to follow") """ + log("trace stuff logger %s dateLogger %s", True) if self.isEnabledFor(self._TRACE): self._log(self._TRACE, msg, args, **kwargs) - def isEnabledFor(self, level): + def xx_isEnabledFor(self, level): """ Is this logger enabled for level 'level'? - currently not modified from logging.Logger class + currently not modified from logging.Logger class, + here only for call log debug. """ log("logger %s isEnabledFor %i>=%i" % (self.name, level, self.getEffectiveLevel())) if self.manager.disable >= level: return 0 return level >= self.getEffectiveLevel() + +################################################################# class DefaultFormatter(LOGI.Formatter): # to set color prefix, problem with indent format as @@ -141,10 +234,11 @@ class DefaultFormatter(LOGI.Formatter): res = color + levelname + "" nb = len(levelname) res = res + " "*(8-nb) # 8 as len("CRITICAL") - # print "'%s'" % res + # log("setColorLevelname'%s'" % res) return res +################################################################# class UnittestFormatter(LOGI.Formatter): def format(self, record): # print "", record.levelname #type(record), dir(record) @@ -154,6 +248,7 @@ class UnittestFormatter(LOGI.Formatter): return COLS.toColor(res) +################################################################# class UnittestStream(object): """ write my stream class @@ -184,7 +279,60 @@ class UnittestStream(object): def __str__(self): return self._logs +################################################################# +class XmlHandler(BufferingHandler): + """ + log outputs in memory as BufferingHandler. + Write ElementTree in file and flush are done once + when method close is called, to generate xml file. + + see: https://docs.python.org/2/library/logging.handlers.html + """ + def __init__(self, capacity): + super(XmlHandler, self).__init__(capacity) + self._target_file = None + self._config = None + + def set_target_file(self, filename): + """ + filename is file name xml with path + supposedly non existing, no overwrite accepted + """ + if os.path.exists(filename): + msg = "XmlHandler target file %s existing yet" % filename + raise Exception(msg) + self._target_file = filename + def set_config(self, config): + """ + config is supposedly non existing, no overwrite accepted + """ + if self._config is not None: + msg = "XmlHandler target config existing yet" + raise Exception(msg) + self._config = config + + def close(self): + """prepare ElementTree from existing logs and write xml file""" + import src.xmlManager as XMLMGR # avoid import cross utilsSat + targetFile = self._target_file + config = self._config + + # TODO for degug + log("XmlHandler to xml file\n%s" % PP.pformat(getListOfStrLogRecord(self.buffer)), True) + + if os.path.exists(targetFile): + msg = "XmlHandler target file %s existing yet" % targetFile + + xmlFile = XMLMGR.XmlLogFile(targetFile, "SATcommand", attrib = {"application" : config.VARS.application}) + xmlFile.write_tree() + + super(XmlHandler, self).close() # zaps the buffer to empty + +################################################################# +# methods to define two LoggerSat instances in salomeTools, +# no more need +################################################################# def initLoggerAsDefault(logger, fmt=None, level=None): """ init logger as prefixed message and indented message if multi line @@ -192,6 +340,7 @@ def initLoggerAsDefault(logger, fmt=None, level=None): """ log("initLoggerAsDefault name=%s\nfmt='%s' level='%s'" % (logger.name, fmt, level)) handler = LOGI.StreamHandler(sys.stdout) # Logging vers console + handler.set_name(logger.name + "_console") if fmt is not None: # formatter = LOGI.Formatter(fmt, "%Y-%m-%d %H:%M:%S") formatter = DefaultFormatter(fmt, "%y-%m-%d %H:%M:%S") @@ -212,6 +361,7 @@ def initLoggerAsUnittest(logger, fmt=None, level=None): log("initLoggerAsUnittest name=%s\nfmt='%s' level='%s'" % (logger.name, fmt, level)) stream = UnittestStream() handler = LOGI.StreamHandler(stream) # Logging vers stream + handler.set_name(logger.name + "_unittest") if fmt is not None: # formatter = LOGI.Formatter(fmt, "%Y-%m-%d %H:%M:%S") formatter = UnittestFormatter(fmt, "%Y-%m-%d %H:%M:%S") @@ -225,6 +375,7 @@ def initLoggerAsUnittest(logger, fmt=None, level=None): else: logger.setLevel(logger.DEBUG) + def setFileHandler(logger, config): """ add file handler to logger to set log files @@ -240,10 +391,56 @@ def setFileHandler(logger, config): | ~/LOGS/OUT/micro_20180510_140607_clean_lenovo.txt | etc. """ - import src.debug as DBG # avoid cross import - DBG.write("setFileHandler", logger.handlers, True) - DBG.write("setFileHandler", config.VARS, True) + #import src.debug as DBG # avoid cross import + log("setFileHandler %s" % logger, True) + log("setFileHandler config\n%s" % PP.pformat(dict(config.VARS)), True) + log("setFileHandler TODO set log_dir config.LOCAL.log_dir", True) + log_dir = "TMP" # TODO for debug config.LOCAL.log_dir # files xml + log_dir_out = os.path.join(log_dir, "OUT") # files txt + UTS.ensure_path_exists(log_dir) + UTS.ensure_path_exists(log_dir_out) + datehour = config.VARS.datehour + cmd = config.VARS.command + hostname = config.VARS.hostname + nameFileXml = "%s_%s_%s.xml" % (datehour, cmd, hostname) + nameFileTxt = "%s_%s_%s.txt" % (datehour, cmd, hostname) + fileXml = os.path.join(log_dir, nameFileXml) + fileTxt = os.path.join(log_dir_out, nameFileTxt) + + nbhandl = len(logger.handlers) # number of current handlers + if nbhandl == 1: # first main command + # Logging vers file xml + + handler = XmlHandler(1000) # log outputs in memory + handler.setLevel(LOGI.INFO) + handler.set_name(nameFileXml) + handler.set_target_file(fileXml) + handler.set_config(config) + + fmt = '%(asctime)s :: %(levelname)s :: %(message)s' + formatter = UnittestFormatter(fmt, "%Y-%m-%d %H:%M:%S") + + handler.setFormatter(formatter) + logger.addHandler(handler) + + # Logging vers file txt + handler = LOGI.FileHandler(fileTxt) + handler.setLevel(LOGI.TRACE) + handler.set_name(nameFileTxt) + + fmt = '%(asctime)s :: %(levelname)s :: %(message)s' + formatter = UnittestFormatter(fmt, "%Y-%m-%d %H:%M:%S") + + handler.setFormatter(formatter) + logger.addHandler(handler) + + if nbhandl > 1: # secondary micro command + log("setFileHandler micro command %s" % config.VARS.command, True) + + log("setFileHandler %s" % logger, True) + + def getDefaultLogger(): log("getDefaultLogger %s" % _loggerDefaultName) # case multithread may be problem as not LOGI._acquireLock() @@ -253,6 +450,7 @@ def getDefaultLogger(): LOGI.setLoggerClass(previousClass) return res + def getUnittestLogger(): log("getUnittestLogger %s" % _loggerUnittestName) # case multithread may be problem as not LOGI._acquireLock() @@ -261,10 +459,14 @@ def getUnittestLogger(): res = LOGI.getLogger(_loggerUnittestName) LOGI.setLoggerClass(previousClass) return res + +################################################################# +# small tests as demonstration, see unittest also +################################################################# def testLogger_1(logger): """small test""" - # dirLogger(logger) + # print getStrDirLogger(logger) logger.debug('test logger debug') logger.trace('test logger trace') logger.info('test logger info') @@ -274,6 +476,7 @@ def testLogger_1(logger): logger.info('\ntest logger info: no indent\n- second line\n- third line\n') logger.warning('test logger warning:\n- second line\n- third line') + def testMain(): print("\n**** DEFAULT logger") logdef = getDefaultLogger() @@ -291,13 +494,21 @@ def testMain(): from colorama import Style as ST print("this is unconditionally %scolored in green%s !!!" % (FG.GREEN, ST.RESET_ALL)) + + +################################################################# +# in production, or not (if __main__) +################################################################# if __name__ == "__main__": + # for example, not in production # get path to salomeTools sources satdir = os.path.dirname(os.path.dirname(__file__)) # Make the src & commands package accessible from all code sys.path.insert(0, satdir) - testMain() + testMain() + # here we have sys.exit() else: + # in production # get two LoggerSat instance used in salomeTools, no more needed. _loggerDefault = getDefaultLogger() _loggerUnittest = getUnittestLogger() diff --git a/src/product.py b/src/product.py index 917197a..c2064ef 100644 --- a/src/product.py +++ b/src/product.py @@ -684,7 +684,7 @@ def product_has_script(product_info): :param product_info: (Config) The configuration specific to the product :return: (bool) - True if the product it has a compilation script, else False + True if the product has a compilation script, else False """ if "build_source" not in product_info: # Native case @@ -698,7 +698,7 @@ def product_has_env_script(product_info): :param product_info: (Config) The configuration specific to the product :return: (bool) - True if the product it has an environment script, else False + True if the product has an environment script, else False """ return "environ" in product_info and "env_script" in product_info.environ diff --git a/src/xmlManager.py b/src/xmlManager.py index d6f9392..8e7a0e5 100644 --- a/src/xmlManager.py +++ b/src/xmlManager.py @@ -17,7 +17,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Utilities to manage write/read xml logging files +Utilities to manage write/read xml salometools logging files | Usage: | >> import src.xmlManager as XMLMGR @@ -35,6 +35,9 @@ import src.ElementTree as ETREE import src.utilsSat as UTS +############################################################################## +# classes to write and read xml salometools logging files +############################################################################## class XmlLogFile(object): """ Class to manage writing in salomeTools xml log file @@ -104,7 +107,163 @@ class XmlLogFile(object): :param attrib: (dict) The attrib to append """ self.xmlroot.find(node_name).attrib.update(attrib) + + + def put_initial_xml_fields(self): + """ + Called at class initialization: Put all fields + corresponding to the command context (user, time, ...) + """ + # command name + self.xmlFile.add_simple_node("Site", attrib={"command" : + self.config.VARS.command}) + # version of salomeTools + self.xmlFile.append_node_attrib("Site", attrib={"satversion" : + self.config.INTERNAL.sat_version}) + # machine name on which the command has been launched + self.xmlFile.append_node_attrib("Site", attrib={"hostname" : + self.config.VARS.hostname}) + # Distribution of the machine + self.xmlFile.append_node_attrib("Site", attrib={"OS" : + self.config.VARS.dist}) + # The user that have launched the command + self.xmlFile.append_node_attrib("Site", attrib={"user" : + self.config.VARS.user}) + # The time when command was launched + Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour) + date_hour = "%2s/%2s/%4s %2sh%2sm%2ss" % (dd, m, Y, H, M, S) + self.xmlFile.append_node_attrib("Site", attrib={"beginTime" : + date_hour}) + # The application if any + if "APPLICATION" in self.config: + self.xmlFile.append_node_attrib("Site", + attrib={"application" : self.config.VARS.application}) + # The initialization of the trace node + self.xmlFile.add_simple_node("Log",text="") + # The system commands logs + self.xmlFile.add_simple_node("OutLog", + text=os.path.join("OUT", self.txtFileName)) + # The initialization of the node where + # to put the links to the other sat commands that can be called by any + # command + self.xmlFile.add_simple_node("Links") + + def add_link(self, + log_file_name, + command_name, + command_res, + full_launched_command): + """Add a link to another log file. + + :param log_file_name str: The file name of the link. + :param command_name str: The name of the command linked. + :param command_res str: The result of the command linked. "0" or "1" + :parma full_launched_command str: The full lanch command + ("sat command ...") + """ + xmlLinks = self.xmlFile.xmlroot.find("Links") + src.xmlManager.add_simple_node(xmlLinks, + "link", + text = log_file_name, + attrib = {"command" : command_name, + "passed" : command_res, + "launchedCommand" : full_launched_command}) + + def write(self, message, level=None, screenOnly=False): + """ + function used in the commands + to print in the terminal and the log file. + + :param message str: The message to print. + :param level int: The output level corresponding + to the message 0 < level < 6. + :param screenOnly boolean: if True, do not write in log file. + """ + # do not write message starting with \r to log file + if not message.startswith("\r") and not screenOnly: + self.xmlFile.append_node_text("Log", + printcolors.cleancolor(message)) + + # get user or option output level + current_output_verbose_level = self.config.USER.output_verbose_level + if not ('isatty' in dir(sys.stdout) and sys.stdout.isatty()): + # clean the message color if the terminal is redirected by user + # ex: sat compile appli > log.txt + message = printcolors.cleancolor(message) + + # Print message regarding the output level value + if level: + if level <= current_output_verbose_level and not self.silentSysStd: + sys.stdout.write(message) + else: + if self.default_level <= current_output_verbose_level and not self.silentSysStd: + sys.stdout.write(message) + self.flush() + def error(self, message): + """Print an error. + + :param message str: The message to print. + """ + # Print in the log file + self.xmlFile.append_node_text("traces", _('ERROR:') + message) + + # Print in the terminal and clean colors if the terminal + # is redirected by user + if not ('isatty' in dir(sys.stderr) and sys.stderr.isatty()): + sys.stderr.write(printcolors.printcError(_('ERROR:') + message)) + else: + sys.stderr.write(_('ERROR:') + message) + + + def end_write(self, attribute): + """ + Called just after command end: Put all fields + corresponding to the command end context (time). + Write the log xml file on the hard drive. + And display the command to launch to get the log + + :param attribute dict: the attribute to add to the node "Site". + """ + # Get current time (end of command) and format it + dt = datetime.datetime.now() + Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour) + t0 = datetime.datetime(int(Y), int(m), int(dd), int(H), int(M), int(S)) + tf = dt + delta = tf - t0 + total_time = timedelta_total_seconds(delta) + hours = int(total_time / 3600) + minutes = int((total_time - hours*3600) / 60) + seconds = total_time - hours*3600 - minutes*60 + # Add the fields corresponding to the end time + # and the total time of command + endtime = dt.strftime('%Y/%m/%d %Hh%Mm%Ss') + self.xmlFile.append_node_attrib("Site", attrib={"endTime" : endtime}) + self.xmlFile.append_node_attrib("Site", + attrib={"TotalTime" : "%ih%im%is" % (hours, minutes, seconds)}) + + # Add the attribute passed to the method + self.xmlFile.append_node_attrib("Site", attrib=attribute) + + # Call the method to write the xml file on the hard drive + self.xmlFile.write_tree(stylesheet = "command.xsl") + + # Dump the config in a pyconf file in the log directory + logDir = src.get_log_path(self.config) + dumpedPyconfFileName = (self.config.VARS.datehour + + "_" + + self.config.VARS.command + + ".pyconf") + dumpedPyconfFilePath = os.path.join(logDir, 'OUT', dumpedPyconfFileName) + try: + f = open(dumpedPyconfFilePath, 'w') + self.config.__save__(f) + f.close() + except IOError: + pass + + +############################################################################## class ReadXmlFile(object): """ Class to manage reading of an xml log file @@ -158,6 +317,9 @@ class ReadXmlFile(object): """ return self.xmlroot.find(node).text +############################################################################## +# utilities method +############################################################################## def add_simple_node(root_node, node_name, text=None, attrib={}): """Add a node with some attibutes and text to the root node. @@ -203,7 +365,6 @@ def find_node_by_attrib(xmlroot, name_node, key, value): return node return None - def write_report(filename, xmlroot, stylesheet): """Writes a report file from a XML tree. diff --git a/test/test_024_logging.py b/test/test_024_logging.py index 7a22651..3102386 100755 --- a/test/test_024_logging.py +++ b/test/test_024_logging.py @@ -127,7 +127,7 @@ class TestCase(unittest.TestCase): # creation d'un handler pour chaque log sur la console formatter = LOGI.Formatter('%(levelname)-8s :: %(message)s') # stream_handler = LOGI.handlers.StreamHandler() # log outputs in console - stream_handler = LOGI.handlers.BufferingHandler(1000) # logoutputs in memory + stream_handler = LOGI.handlers.BufferingHandler(1000) # log outputs in memory stream_handler.setLevel(LOGI.DEBUG) stream_handler.setFormatter(formatter) lgr.addHandler(stream_handler)