2 # -*- coding: utf-8 -*-
5 salomeTools logger. using logging package
7 | http://sametmax.com/ecrire-des-logs-en-python/
9 | Define two LoggerSat instances in salomeTools, no more need.
10 | - _loggerDefault as production/development logger
11 | - _loggerUnittest as unittest logger
21 import logging as LOGI
23 from logging.handlers import BufferingHandler
26 import src.debug as DBG # Easy print stderr (for DEBUG only)
29 _name = "loggingSimple"
30 _loggerDefaultName = 'SimpleDefaultLogger'
31 _loggerUnittestName = 'SimpleUnittestLogger'
33 _STEP = LOGI.INFO - 1 # step level is just below INFO
34 _TRACE = LOGI.INFO - 2 # trace level is just below STEP
36 LOGI.STEP = _STEP # only for coherency,
37 LOGI.TRACE = _TRACE # only for coherency,
39 _knownLevels = "CRITICAL ERROR WARNING INFO STEP TRACE DEBUG".upper().split()
40 _knownLevelsStr = "[%s]" % "|".join(_knownLevels)
43 #################################################################
45 #################################################################
47 def filterLevel(aLevel):
49 filter levels logging values from firsts characters levels.
54 | 'cRiT' -> 'CRITICAL'
57 knownLevels = _knownLevels
58 maxLen = max([len(i) for i in knownLevels])
59 for i in range(maxLen):
60 for lev in knownLevels:
62 # DBG.write("filterLevel", "%s -> %s" % (aLevel, lev))
64 msg = "Unknown level '%s', accepted are:\n%s" % (aLev, ", ".join(knownLevels))
66 # raise Exception(msg)
69 def indent(msg, nb, car=" "):
70 """indent nb car (spaces) multi lines message except first one"""
72 res = ("\n" + car * nb).join(s)
76 def indentUnittest(msg, prefix=" | "):
78 indent multi lines message except first one with prefix.
79 prefix default is designed for less spaces for size logs files
80 and keep logs human eye readable
83 res = ("\n" + prefix).join(s)
87 def log(msg, force=False):
88 """elementary log when no logging.Logger yet"""
89 prefix = "---- %s.log: " % _name
92 print(prefix + indent(msg, nb))
95 # just for debug info where is import logging
96 log("import logging on %s" % LOGI.__file__)
99 def getStrDirLogger(logger):
101 Returns multi line string for logger description, with dir(logger).
104 lgr = logger # shortcut
105 msg = "%s(name=%s, dateLogger=%s):\n%s\n"
106 cName = lgr.__class__.__name__
107 res = msg % (cName, lgr.name, lgr.dateLogger, PP.pformat(dir(lgr)))
111 def getStrHandler(handler):
113 Returns one line string for handler description
114 (as inexisting __repr__)
115 to avoid create inherited classe(s) handler
117 h = handler # shortcut
119 cName = h.__class__.__name__
120 res = msg % (cName, h.get_name())
124 def getStrShort(msg):
125 """Returns short string for msg (as first caracters without line feed"""
126 # log("getStrShort " + str(msg), True)
127 res = msg.replace("\n", "//")[0:30]
131 def getStrLogRecord(logRecord):
133 Returns one line string for simple logging LogRecord description
135 msg = "LogRecord(level='%s', msg='%s...')"
136 shortMsg = getStrShort(logRecord.msg)
137 levelName = logRecord.levelname
138 res = msg % (levelName, shortMsg)
142 def getListOfStrLogRecord(listOfLogRecord):
144 Returns one line string for logging LogRecord description
146 res = [getStrLogRecord(l) for l in listOfLogRecord]
150 #################################################################
151 # salometools logger classes
152 #################################################################
161 def getMessage(self):
163 modified from logging.__init__.LogRecord.getMessage,
164 better message on format error
165 Return the message for this LogRecord.
167 Return the message for this LogRecord after merging any user-supplied
168 arguments with the message.
170 if not _unicode: # if no unicode support...
174 if not isinstance(msg, basestring):
178 msg = self.msg # Defer encoding till later
180 try: # better message on format error
181 msg = msg % self.args
182 except Exception as e:
183 msg = "ERROR: %s with args %s" % (msg, PP.pformat(self.args))
188 LOGI.LogRecord.getMessage = getMessage # better message if error
191 #################################################################
192 class LoggerSimple(LOGI.Logger):
194 Inherited class logging.Logger for logger salomeTools
196 | add a level STEP as log.step(msg)
197 | add a level TRACE as log.trace(msg)
198 | below log.info(msg)
199 | above log.debug(msg)
200 | to assume message step inside files xml 'command's internal traces'
201 | to assume store long log asci in files txt outside files xml
203 | see: /usr/lib64/python2.7/logging/__init__.py etc.
206 def __init__(self, name, level=LOGI.INFO):
208 Initialize the logger with a name and an optional level.
210 super(LoggerSimple, self).__init__(name, level)
211 LOGI.addLevelName(_STEP, "STEP")
212 LOGI.addLevelName(_TRACE, "TRACE")
213 self.dateLogger = "NoDateLogger"
214 self.dateHour = None # datehour of main command
215 self.isClosed = False
221 final stuff for logger, done at end salomeTools
222 flushed and closed xml files have to be not overriden/appended
225 raise Exception("logger closed yet: %s" % self)
226 log("close stuff logger %s" % self) # getStrDirLogger(self)
227 for handl in list(self.handlers): # get original list
228 log("close stuff handler %s" % getStrHandler(handl))
229 handl.close() # Tidy up any resources used by the handler.
230 self.removeHandler(handl)
232 self.isClosed = True # done at end of execution
236 """one line string representation"""
237 msg = "%s(name=%s, dateLogger=%s, handlers=%s)"
238 cName = self.__class__.__name__
239 h = [getStrHandler(h) for h in self.handlers]
240 h = "[" + ", ".join(h) + "]"
241 res = msg % (cName, self.name, self.dateLogger, h)
244 def trace(self, msg, *args, **kwargs):
246 Log 'msg % args' with severity '_TRACE'.
248 log("trace stuff logger '%s' msg '%s...'" % (self.name, getStrShort(msg)))
249 if self.isEnabledFor(_TRACE):
250 self._log(_TRACE, msg, args, **kwargs)
252 def step(self, msg, *args, **kwargs):
254 Log 'msg % args' with severity '_STEP'.
256 log("step stuff logger '%s' msg '%s...'" % (self.name, getStrShort(msg)))
257 if self.isEnabledFor(_STEP):
258 self._log(_STEP, msg, args, **kwargs)
260 def setLevelMainHandler(self, level):
261 handl = self.handlers[0] # get main handler
262 log("setLevelMainHandler %s" % level)
263 handl.setLevel(level)
266 #################################################################
267 class UnittestFormatter(LOGI.Formatter):
269 this formatter prefixes level name and indents all messages
271 def format(self, record):
272 # print "", record.levelname #type(record), dir(record)
273 # nb = len("2018-03-17 12:15:41 :: INFO :: ")
274 res = super(UnittestFormatter, self).format(record)
275 res = indentUnittest(res)
278 #################################################################
279 class DefaultFormatter(LOGI.Formatter):
281 this formatter prefixes level name and indents all messages but INFO stay "as it"
283 def format(self, record):
284 # print "", record.levelname #type(record), dir(record)
285 # nb = len("2018-03-17 12:15:41 :: INFO :: ")
286 if record.levelname == "INFO":
287 res = record.getMessage()
289 res = super(DefaultFormatter, self).format(record)
290 res = indentUnittest(res)
294 #################################################################
295 class UnittestStream(object):
297 write my stream class
298 only write and flush are used for the streaming
300 | https://docs.python.org/2/library/logging.handlers.html
301 | https://stackoverflow.com/questions/31999627/storing-logger-messages-in-a-string
310 def getLogsAndClear(self):
315 def write(self, astr):
316 """final method called when message is logged"""
317 # log("UnittestStream.write('%s')" % astr, True) # for debug ...
327 #################################################################
328 class StreamHandlerSimple(LOGI.StreamHandler):
330 A handler class which writes logging records, appropriately formatted,
331 to a stream. Note that this class does not close the stream, as
332 sys.stdout or sys.stderr may be used.
334 from logging.StreamHandler class,
335 modified for 'no return' mode line if '...' at end of record message
338 def emit(self, record):
342 If a formatter is specified, it is used to format the record.
343 The record is then written to the stream with a trailing newline. If
344 exception information is present, it is formatted using
345 traceback.print_exception and appended to the stream. If the stream
346 has an 'encoding' attribute, it is used to determine how to do the
347 output to the stream.
349 # log("StreamHandlerSimple.emit('%s')" % record, True) # for debug ...
351 msg = self.format(record)
355 if not _unicode: # if no unicode support...
356 stream.write(fs % msg)
359 if (isinstance(msg, unicode) and
360 getattr(stream, 'encoding', None)):
363 stream.write(ufs % msg)
364 except UnicodeEncodeError:
365 # Printing to terminals sometimes fails. For example,
366 # with an encoding of 'cp1251', the above write will
367 # work if written to a stream opened or wrapped by
368 # the codecs module, but fail when writing to a
369 # terminal even when the codepage is set to cp1251.
370 # An extra encoding step seems to be needed.
371 stream.write((ufs % msg).encode(stream.encoding))
373 stream.write(fs % msg)
375 stream.write(fs % msg.encode("UTF-8"))
377 except (KeyboardInterrupt, SystemExit):
380 self.handleError(record)
384 #################################################################
385 # methods to define two LoggerSimple instances in salomeTools,
387 #################################################################
388 def initLoggerAsDefault(logger, fmt=None, level=None):
390 init logger as prefixed message and indented message if multi line
391 exept info() outed 'as it' without any format.
392 level could be modified during execution
394 log("initLoggerAsDefault name=%s\nfmt='%s' level='%s'" % (logger.name, fmt, level))
395 #handler = StreamHandlerSimple(sys.stdout) # Logging vers console
396 handler = LOGI.StreamHandler(sys.stdout) # Logging vers console
397 handler.set_name(logger.name + "_console")
399 # formatter = UnittestFormatter(fmt, "%y-%m-%d %H:%M:%S")
400 formatter = DefaultFormatter(fmt, "%y-%m-%d %H:%M:%S")
401 handler.setFormatter(formatter)
402 handler.idCommandHandlers = 0
403 logger.addHandler(handler)
404 # as RootLogger is level WARNING
405 # my logger is not notset but low, handlers needs setlevel greater
406 logger.setLevel(LOGI.DEBUG)
407 # import src/debug as DBG
408 # tmp = (logger.getEffectiveLevel(), LOGI.NOTSET, logger.level, logger.parent.level)
409 # DBG.write("logger levels tmp, True)
410 if level is not None: # level could be modified during execution
411 handler.setLevel(level) # on screen log as user wants
413 handler.setLevel(LOGI.STEP) # on screen no log step, which are in xml files
417 def initLoggerAsUnittest(logger, fmt=None, level=None):
419 init logger as silent on stdout/stderr
420 used for retrieve messages in memory for post execution unittest
421 https://docs.python.org/2/library/logging.handlers.html
423 log("initLoggerAsUnittest name=%s\nfmt='%s' level='%s'" % (logger.name, fmt, level))
424 stream = UnittestStream()
425 handler = LOGI.StreamHandler(stream) # Logging vers stream
426 handler.set_name(logger.name + "_unittest")
428 # formatter = LOGI.Formatter(fmt, "%Y-%m-%d %H:%M:%S")
429 formatter = UnittestFormatter(fmt, "%Y-%m-%d %H:%M:%S")
430 handler.setFormatter(formatter)
431 handler.idCommandHandlers = 0
432 logger.addHandler(handler)
433 logger.stream = stream
434 logger.getLogs = stream.getLogs
435 logger.getLogsAndClear = stream.getLogsAndClear
436 if level is not None:
437 logger.setLevel(level)
439 logger.setLevel(LOGI.DEBUG)
442 def getDefaultLogger():
443 log("getDefaultLogger %s" % _loggerDefaultName)
444 # case multithread may be problem as not LOGI._acquireLock()
445 previousClass = LOGI._loggerClass
446 LOGI.setLoggerClass(LoggerSimple) # to get LoggerSimple instance with trace etc.
447 res = LOGI.getLogger(_loggerDefaultName)
448 LOGI.setLoggerClass(previousClass)
452 def getUnittestLogger():
453 log("getUnittestLogger %s" % _loggerUnittestName)
454 # case multithread may be problem as not LOGI._acquireLock()
455 previousClass = LOGI._loggerClass
456 LOGI.setLoggerClass(LoggerSimple) # to get LoggerSimple instance with trace etc.
457 res = LOGI.getLogger(_loggerUnittestName)
458 LOGI.setLoggerClass(previousClass)
462 #################################################################
463 # small tests as demonstration, see unittest also
464 #################################################################
465 def testLogger_2(logger):
467 # print getStrDirLogger(logger)
468 logger.debug('test logger debug')
469 logger.trace('test logger trace')
470 logger.info('test logger info')
471 logger.warning('test logger warning')
472 logger.error('test logger error')
473 logger.critical('test logger critical')
474 logger.info('\ntest logger info:\n- second line\n- third line\n')
475 logger.warning('test logger warning:\n- second line\n- third line')
478 print("\n**** DEFAULT logger")
479 logdef = getDefaultLogger()
480 # use of setColorLevelname <color>...<reset>, so do not use %(levelname)-8s
481 initLoggerAsDefault(logdef, '%(levelname)-8s :: %(message)s', level=LOGI.DEBUG)
484 print("\n**** UNITTEST logger")
485 loguni = getUnittestLogger()
486 initLoggerAsUnittest(loguni, '%(asctime)s :: %(levelname)-8s :: %(message)s', level=LOGI.DEBUG)
487 testLogger_2(loguni) # is silent
488 # log("loguni.getLogs():\n%s" % loguni.getLogs())
489 print("loguni.streamUnittest:\n%s" % loguni.getLogs())
492 #################################################################
493 # in production, or not (if __main__)
494 #################################################################
495 if __name__ == "__main__":
496 # for example, not in production
497 # get path to salomeTools sources
498 curdir = os.path.dirname(os.path.dirname(__file__))
499 # Make the src & commands package accessible from all code
500 sys.path.insert(0, curdir)
502 # here we have sys.exit()
505 # get two LoggerSat instance used in salomeTools, no more needed.
506 _loggerDefault = getDefaultLogger()
507 _loggerUnittest = getUnittestLogger()
508 initLoggerAsDefault(_loggerDefault, '%(levelname)-8s :: %(message)s')
509 initLoggerAsUnittest(_loggerUnittest, '%(asctime)s :: %(levelname)s :: %(message)s')