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 # get_name absent in logging 0.5.0.5 python 2.6
121 res = msg % (cName, h._name)
125 def getStrShort(msg):
126 """Returns short string for msg (as first caracters without line feed"""
127 # log("getStrShort " + str(msg), True)
128 res = msg.replace("\n", "//")[0:30]
132 def getStrLogRecord(logRecord):
134 Returns one line string for simple logging LogRecord description
136 msg = "LogRecord(level='%s', msg='%s...')"
137 shortMsg = getStrShort(logRecord.msg)
138 levelName = logRecord.levelname
139 res = msg % (levelName, shortMsg)
143 def getListOfStrLogRecord(listOfLogRecord):
145 Returns one line string for logging LogRecord description
147 res = [getStrLogRecord(l) for l in listOfLogRecord]
151 #################################################################
152 # salometools logger classes
153 #################################################################
162 def getMessage(self):
164 modified from logging.__init__.LogRecord.getMessage,
165 better message on format error
166 Return the message for this LogRecord.
168 Return the message for this LogRecord after merging any user-supplied
169 arguments with the message.
171 if not _unicode: # if no unicode support...
175 if not isinstance(msg, basestring):
179 msg = self.msg # Defer encoding till later
181 try: # better message on format error
182 msg = msg % self.args
183 except Exception as e:
184 msg = "ERROR: %s with args %s" % (msg, PP.pformat(self.args))
189 LOGI.LogRecord.getMessage = getMessage # better message if error
192 #################################################################
193 class LoggerSimple(LOGI.Logger, object): # object force new-style classes in logging 0.5.0.5 python 2.6
195 Inherited class logging.Logger for logger salomeTools
197 | add a level STEP as log.step(msg)
198 | add a level TRACE as log.trace(msg)
199 | below log.info(msg)
200 | above log.debug(msg)
201 | to assume message step inside files xml 'command's internal traces'
202 | to assume store long log asci in files txt outside files xml
204 | see: /usr/lib64/python2.7/logging/__init__.py etc.
207 def __init__(self, name, level=LOGI.INFO):
209 Initialize the logger with a name and an optional level.
211 super(LoggerSimple, self).__init__(name, level)
212 LOGI.addLevelName(_STEP, "STEP")
213 LOGI.addLevelName(_TRACE, "TRACE")
214 self.dateLogger = "NoDateLogger"
215 self.dateHour = None # datehour of main command
216 self.isClosed = False
222 final stuff for logger, done at end salomeTools
223 flushed and closed xml files have to be not overriden/appended
226 raise Exception("logger closed yet: %s" % self)
227 log("close stuff logger %s" % self) # getStrDirLogger(self)
228 for handl in list(self.handlers): # get original list
229 log("close stuff handler %s" % getStrHandler(handl))
230 handl.close() # Tidy up any resources used by the handler.
231 self.removeHandler(handl)
233 self.isClosed = True # done at end of execution
237 """one line string representation"""
238 msg = "%s(name=%s, dateLogger=%s, handlers=%s)"
239 cName = self.__class__.__name__
240 h = [getStrHandler(h) for h in self.handlers]
241 h = "[" + ", ".join(h) + "]"
242 res = msg % (cName, self.name, self.dateLogger, h)
245 def trace(self, msg, *args, **kwargs):
247 Log 'msg % args' with severity '_TRACE'.
249 log("trace stuff logger '%s' msg '%s...'" % (self.name, getStrShort(msg)))
250 if self.isEnabledFor(_TRACE):
251 self._log(_TRACE, msg, args, **kwargs)
253 def step(self, msg, *args, **kwargs):
255 Log 'msg % args' with severity '_STEP'.
257 log("step stuff logger '%s' msg '%s...'" % (self.name, getStrShort(msg)))
258 if self.isEnabledFor(_STEP):
259 self._log(_STEP, msg, args, **kwargs)
261 def setLevelMainHandler(self, level):
262 handl = self.handlers[0] # get main handler
263 log("setLevelMainHandler %s" % level)
264 handl.setLevel(level)
267 #################################################################
268 class UnittestFormatter(LOGI.Formatter, object): # object force new-style classes in logging 0.5.0.5 python 2.6
270 this formatter prefixes level name and indents all messages
272 def format(self, record):
273 # print "", record.levelname #type(record), dir(record)
274 # nb = len("2018-03-17 12:15:41 :: INFO :: ")
275 res = super(UnittestFormatter, self).format(record)
276 res = indentUnittest(res)
279 #################################################################
280 class DefaultFormatter(LOGI.Formatter, object): # object force new-style classes in logging 0.5.0.5 python 2.6
282 this formatter prefixes level name and indents all messages but INFO stay "as it"
284 def format(self, record):
285 # print "", record.levelname #type(record), dir(record)
286 # nb = len("2018-03-17 12:15:41 :: INFO :: ")
287 if record.levelname == "INFO":
288 res = record.getMessage()
290 res = super(DefaultFormatter, self).format(record)
291 res = indentUnittest(res)
295 #################################################################
296 class UnittestStream(object):
298 write my stream class
299 only write and flush are used for the streaming
301 | https://docs.python.org/2/library/logging.handlers.html
302 | https://stackoverflow.com/questions/31999627/storing-logger-messages-in-a-string
311 def getLogsAndClear(self):
316 def write(self, astr):
317 """final method called when message is logged"""
318 # log("UnittestStream.write('%s')" % astr, True) # for debug ...
328 #################################################################
329 class StreamHandlerSimple(LOGI.StreamHandler, object): # object force new-style classes in logging 0.5.0.5 python 2.6
331 A handler class which writes logging records, appropriately formatted,
332 to a stream. Note that this class does not close the stream, as
333 sys.stdout or sys.stderr may be used.
335 from logging.StreamHandler class,
336 modified for 'no return' mode line if '...' at end of record message
339 def emit(self, record):
343 If a formatter is specified, it is used to format the record.
344 The record is then written to the stream with a trailing newline. If
345 exception information is present, it is formatted using
346 traceback.print_exception and appended to the stream. If the stream
347 has an 'encoding' attribute, it is used to determine how to do the
348 output to the stream.
350 # log("StreamHandlerSimple.emit('%s')" % record, True) # for debug ...
352 msg = self.format(record)
356 if not _unicode: # if no unicode support...
357 stream.write(fs % msg)
360 if (isinstance(msg, unicode) and
361 getattr(stream, 'encoding', None)):
364 stream.write(ufs % msg)
365 except UnicodeEncodeError:
366 # Printing to terminals sometimes fails. For example,
367 # with an encoding of 'cp1251', the above write will
368 # work if written to a stream opened or wrapped by
369 # the codecs module, but fail when writing to a
370 # terminal even when the codepage is set to cp1251.
371 # An extra encoding step seems to be needed.
372 stream.write((ufs % msg).encode(stream.encoding))
374 stream.write(fs % msg)
376 stream.write(fs % msg.encode("UTF-8"))
378 except (KeyboardInterrupt, SystemExit):
381 self.handleError(record)
385 #################################################################
386 # methods to define two LoggerSimple instances in salomeTools,
388 #################################################################
389 def initLoggerAsDefault(logger, fmt=None, level=None):
391 init logger as prefixed message and indented message if multi line
392 exept info() outed 'as it' without any format.
393 level could be modified during execution
395 log("initLoggerAsDefault name=%s\nfmt='%s' level='%s'" % (logger.name, fmt, level))
396 #handler = StreamHandlerSimple(sys.stdout) # Logging vers console
397 handler = LOGI.StreamHandler(sys.stdout) # Logging vers console
398 # set_name absent in logging 0.5.0.5 python 2.6
399 handler._name = logger.name + "_console"
401 # formatter = UnittestFormatter(fmt, "%y-%m-%d %H:%M:%S")
402 formatter = DefaultFormatter(fmt, "%y-%m-%d %H:%M:%S")
403 handler.setFormatter(formatter)
404 handler.idCommandHandlers = 0
405 logger.addHandler(handler)
406 # as RootLogger is level WARNING
407 # my logger is not notset but low, handlers needs setlevel greater
408 logger.setLevel(LOGI.DEBUG)
409 # import src/debug as DBG
410 # tmp = (logger.getEffectiveLevel(), LOGI.NOTSET, logger.level, logger.parent.level)
411 # DBG.write("logger levels tmp, True)
412 if level is not None: # level could be modified during execution
413 handler.setLevel(level) # on screen log as user wants
415 handler.setLevel(LOGI.INFO) # on screen no log step, which are in xml files
419 def initLoggerAsUnittest(logger, fmt=None, level=None):
421 init logger as silent on stdout/stderr
422 used for retrieve messages in memory for post execution unittest
423 https://docs.python.org/2/library/logging.handlers.html
425 log("initLoggerAsUnittest name=%s\nfmt='%s' level='%s'" % (logger.name, fmt, level))
426 stream = UnittestStream()
427 handler = LOGI.StreamHandler(stream) # Logging vers stream
428 # set_name absent in logging 0.5.0.5 python 2.6
429 handler._name = logger.name + "_unittest"
431 # formatter = LOGI.Formatter(fmt, "%Y-%m-%d %H:%M:%S")
432 formatter = UnittestFormatter(fmt, "%Y-%m-%d %H:%M:%S")
433 handler.setFormatter(formatter)
434 handler.idCommandHandlers = 0
435 logger.addHandler(handler)
436 logger.stream = stream
437 logger.getLogs = stream.getLogs
438 logger.getLogsAndClear = stream.getLogsAndClear
439 if level is not None:
440 logger.setLevel(level)
442 logger.setLevel(LOGI.DEBUG)
445 def getDefaultLogger():
446 log("getDefaultLogger %s" % _loggerDefaultName)
447 # case multithread may be problem as not LOGI._acquireLock()
448 previousClass = LOGI._loggerClass
449 LOGI.setLoggerClass(LoggerSimple) # to get LoggerSimple instance with trace etc.
450 res = LOGI.getLogger(_loggerDefaultName)
451 LOGI.setLoggerClass(previousClass)
455 def getUnittestLogger():
456 log("getUnittestLogger %s" % _loggerUnittestName)
457 # case multithread may be problem as not LOGI._acquireLock()
458 previousClass = LOGI._loggerClass
459 LOGI.setLoggerClass(LoggerSimple) # to get LoggerSimple instance with trace etc.
460 res = LOGI.getLogger(_loggerUnittestName)
461 LOGI.setLoggerClass(previousClass)
465 #################################################################
466 # small tests as demonstration, see unittest also
467 #################################################################
468 def testLogger_2(logger):
470 # print getStrDirLogger(logger)
471 logger.debug('test logger debug')
472 logger.trace('test logger trace')
473 logger.info('test logger info')
474 logger.warning('test logger warning')
475 logger.error('test logger error')
476 logger.critical('test logger critical')
477 logger.info('\ntest logger info:\n- second line\n- third line\n')
478 logger.warning('test logger warning:\n- second line\n- third line')
481 print("\n**** DEFAULT logger")
482 logdef = getDefaultLogger()
483 # use of setColorLevelname <color>...<reset>, so do not use %(levelname)-8s
484 initLoggerAsDefault(logdef, '%(levelname)-8s :: %(message)s', level=LOGI.DEBUG)
487 print("\n**** UNITTEST logger")
488 loguni = getUnittestLogger()
489 initLoggerAsUnittest(loguni, '%(asctime)s :: %(levelname)-8s :: %(message)s', level=LOGI.DEBUG)
490 testLogger_2(loguni) # is silent
491 # log("loguni.getLogs():\n%s" % loguni.getLogs())
492 print("loguni.streamUnittest:\n%s" % loguni.getLogs())
495 #################################################################
496 # in production, or not (if __main__)
497 #################################################################
498 if __name__ == "__main__":
499 # for example, not in production
500 # get path to salomeTools sources
501 curdir = os.path.dirname(os.path.dirname(__file__))
502 # Make the src & commands package accessible from all code
503 sys.path.insert(0, curdir)
505 # here we have sys.exit()
508 # get two LoggerSat instance used in salomeTools, no more needed.
509 _loggerDefault = getDefaultLogger()
510 _loggerUnittest = getUnittestLogger()
511 initLoggerAsDefault(_loggerDefault, '%(levelname)-8s :: %(message)s')
512 initLoggerAsUnittest(_loggerUnittest, '%(asctime)s :: %(levelname)s :: %(message)s')