Module Gnumed.pycommon.gmLog2
GNUmed logging framework setup.
All error logging, user notification and otherwise unhandled exception handling should go through classes or functions of this module.
Theory of operation:
This module tailors the standard logging framework to the needs of GNUmed.
By importing gmLog2 into your code you'll get the root logger send to a unicode file with messages in a format useful for debugging. The filename is either taken from the command line (–log-file=…) or derived from the name of the main application.
The log file will be found in one of the following standard locations:
1) given on the command line as "–log-file=LOGFILE"
2) ~/.
where
If you want to specify just a directory for the log file you must end the –log-file definition with a slash.
By importing "logging" and getting a logger your modules never need to worry about the real message destination or whether at any given time there's a valid logger available.
Your MAIN module simply imports gmLog2 and all other modules will merrily and automagically start logging away.
Ad hoc call stack logging recipe:
call_stack = inspect.stack()
call_stack.reverse()
for idx in range(1, len(call_stack)):
caller = call_stack[idx]
_log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
del call_stack
Expand source code
"""GNUmed logging framework setup.
All error logging, user notification and otherwise unhandled
exception handling should go through classes or functions of
this module.
Theory of operation:
This module tailors the standard logging framework to
the needs of GNUmed.
By importing gmLog2 into your code you'll get the root
logger send to a unicode file with messages in a format useful
for debugging. The filename is either taken from the
command line (--log-file=...) or derived from the name
of the main application.
The log file will be found in one of the following standard
locations:
1) given on the command line as "--log-file=LOGFILE"
2) ~/.<base_name>/<base_name>.log
3) /dir/of/binary/<base_name>.log (mainly for DOS/Windows)
where <base_name> is derived from the name
of the main application.
If you want to specify just a directory for the log file you
must end the --log-file definition with a slash.
By importing "logging" and getting a logger your modules
never need to worry about the real message destination or whether
at any given time there's a valid logger available.
Your MAIN module simply imports gmLog2 and all other modules
will merrily and automagically start logging away.
Ad hoc call stack logging recipe:
call_stack = inspect.stack()
call_stack.reverse()
for idx in range(1, len(call_stack)):
caller = call_stack[idx]
_log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
del call_stack
"""
# TODO:
# - exception()
# - ascii_ctrl2mnemonic()
#========================================================================
__author__ = "K. Hilbert <Karsten.Hilbert@gmx.net>"
__license__ = "GPL v2 or later (details at https://www.gnu.org)"
# stdlib
import logging
import sys
import os
import datetime as pydt
import random
_logfile_name = None
_logfile = None
_logfile_name_printed = False
# table used for cooking non-printables
AsciiName = ['<#0-0x00-nul>',
'<#1-0x01-soh>',
'<#2-0x02-stx>',
'<#3-0x03-etx>',
'<#4-0x04-eot>',
'<#5-0x05-enq>',
'<#6-0x06-ack>',
'<#7-0x07-bel>',
'<#8-0x08-bs>',
'<#9-0x09-ht>',
'<#10-0x0A-lf>',
'<#11-0x0B-vt>',
'<#12-0x0C-ff>',
'<#13-0x0D-cr>',
'<#14-0x0E-so>',
'<#15-0x0F-si>',
'<#16-0x10-dle>',
'<#17-0x11-dc1/xon>',
'<#18-0x12-dc2>',
'<#19-0x13-dc3/xoff>',
'<#20-0x14-dc4>',
'<#21-0x15-nak>',
'<#22-0x16-syn>',
'<#23-0x17-etb>',
'<#24-0x18-can>',
'<#25-0x19-em>',
'<#26-0x1A-sub>',
'<#27-0x1B-esc>',
'<#28-0x1C-fs>',
'<#29-0x1D-gs>',
'<#30-0x1E-rs>',
'<#31-0x1F-us>'
]
# msg = reduce(lambda x, y: x+y, (map(self.__char2AsciiName, list(tmp))), '')
#
# def __char2AsciiName(self, aChar):
# try:
# return AsciiName[ord(aChar)]
# except IndexError:
# return aChar
#
# def __tracestack(self):
# """extract data from the current execution stack
#
# this is rather fragile, I guess
# """
# stack = traceback.extract_stack()
# self.__modulename = stack[-4][0]
# self.__linenumber = stack[-4][1]
# self.__functionname = stack[-4][2]
# if (self.__functionname == "?"):
# self.__functionname = "Main"
#===============================================================
# external API
#===============================================================
def flush():
"""Log a <synced> line and flush handlers."""
logger = logging.getLogger('gm.logging')
logger.critical('-------- synced log file -------------------------------')
root_logger = logging.getLogger()
for handler in root_logger.handlers:
handler.flush()
#===============================================================
def log_instance_state(instance):
"""Log the state of a class instance."""
logger = logging.getLogger('gm.logging')
logger.debug('state of %s', instance)
for attr in [ a for a in dir(instance) if not a.startswith('__') ]:
try:
val = getattr(instance, attr)
except AttributeError:
val = '<cannot access>'
logger.debug(' %s: %s', attr, val)
#===============================================================
def log_stack_trace(message:str=None, t=None, v=None, tb=None):
"""Log exception details and stack trace.
(t,v,tb) are what sys.exc_info() returns.
If any of (t,v,tb) is None it is attempted to be
retrieved from sys.exc_info().
Args:
message: arbitrary message to add in
t: an exception type
v: an exception value
tb: a traceback object
"""
logger = logging.getLogger('gm.logging')
if t is None:
t = sys.exc_info()[0]
if v is None:
v = sys.exc_info()[1]
if tb is None:
tb = sys.exc_info()[2]
if tb is None:
logger.debug('sys.exc_info() did not return a traceback object, trying sys.last_traceback')
try:
tb = sys.last_traceback
except AttributeError:
logger.debug('no stack to trace (no exception information available)')
return
# log exception details
logger.debug('exception: %s', v)
logger.debug('type: %s', t)
logger.debug('list of attributes:')
for attr in [ a for a in dir(v) if not a.startswith('__') ]:
try:
val = getattr(v, attr)
except AttributeError:
val = '<cannot access>'
logger.debug(' %s: %s', attr, val)
# make sure we don't leave behind a binding
# to the traceback as warned against in
# sys.exc_info() documentation
try:
# recurse back to root caller
while 1:
if not tb.tb_next:
break
tb = tb.tb_next
# put the frames on a stack
stack_of_frames = []
frame = tb.tb_frame
while frame:
stack_of_frames.append(frame)
frame = frame.f_back
finally:
del tb
stack_of_frames.reverse()
if message is not None:
logger.debug(message)
logger.debug('stack trace follows:')
logger.debug('(locals by frame, outmost frame first)')
for frame in stack_of_frames:
logger.debug (
'--- frame [%s]: #%s, %s -------------------',
frame.f_code.co_name,
frame.f_lineno,
frame.f_code.co_filename
)
for varname, value in frame.f_locals.items():
if varname == '__doc__':
continue
logger.debug('%20s = %s', varname, value)
#---------------------------------------------------------------
def log_multiline(level, message:str=None, line_prefix:str=None, text:str=None):
"""Log multi-line text in a standard format.
Args:
level: a log level
message: an arbitrary message to add in
line_prefix: a string to prefix lines with
text: the multi-line text to log
"""
if text is None:
return
if message is None:
message = 'multiline text:'
if line_prefix is None:
line_template = ' > %s'
else:
line_template = '%s: %%s' % line_prefix
lines2log = [message]
lines2log.extend([ line_template % line for line in text.split('\n') ])
logger = logging.getLogger('gm.logging')
logger.log(level, '\n'.join(lines2log))
#---------------------------------------------------------------
__current_log_step = 1
def log_step(level:int=logging.DEBUG, message:str=None, restart:bool=False):
if restart:
global __current_log_step
__current_log_step = 1
logger = logging.getLogger('gm.logging')
if message:
logger.log(level, '%s - %s', __current_log_step, message)
else:
logger.log(level, '%s', __current_log_step)
__current_log_step += 1
#---------------------------------------------------------------
def print_logfile_name():
global _logfile_name_printed
if _logfile_name_printed:
return
print('Log file:', _logfile_name)
_logfile_name_printed = True
#===============================================================
# internal API
#===============================================================
__words2hide = []
def add_word2hide(word:str):
"""Add a string to the list of strings to be scrubbed from logging output.
Useful for hiding credentials etc.
"""
if word is None:
return
if word.strip() == '':
return
if word not in __words2hide:
__words2hide.append(str(word))
#---------------------------------------------------------------
__original_logger_write_func = None
def __safe_logger_write_func(s):
for word in __words2hide:
# throw away up to 4 bits (plus the randint() cost)
random.getrandbits(random.randint(1, 4))
# from there generate a replacement string valid for
# *this* round of replacements of *this* word,
# this approach won't mitigate guessing trivial passwords
# from replacements of known data (a known-plaintext attack)
# but will make automated searching for replaced strings
# in the log more difficult
bummer = hex(random.randint(0, sys.maxsize)).lstrip('0x')
s = s.replace(word, bummer)
__original_logger_write_func(s)
#---------------------------------------------------------------
def __setup_logging():
global _logfile
if _logfile is not None:
return True
if not __get_logfile_name():
return False
_logfile = open(_logfile_name, mode = 'wt', encoding = 'utf8', errors = 'replace')
global __original_logger_write_func
__original_logger_write_func = _logfile.write
_logfile.write = __safe_logger_write_func
# setup
fmt = '%(asctime)s %(levelname)-8s %(name)-12s [%(thread)d %(threadName)-10s] (%(pathname)s::%(funcName)s() #%(lineno)d): %(message)s'
logging.basicConfig (
format = fmt,
datefmt = '%Y-%m-%d %H:%M:%S',
level = logging.DEBUG,
stream = _logfile
)
logging.captureWarnings(True)
logger = logging.getLogger()
# start logging
#logger = logging.getLogger('gm.logging')
logger.critical('-------- start of logging ------------------------------')
logger.info('log file is <%s>', _logfile_name)
logger.info('log level is [%s]', logging.getLevelName(logger.getEffectiveLevel()))
logger.info('log file encoding is <utf8>')
logger.debug('log file .write() patched from original %s to patched %s', __original_logger_write_func, __safe_logger_write_func)
#---------------------------------------------------------------
def __get_logfile_name():
global _logfile_name
if _logfile_name is not None:
return _logfile_name
def_log_basename = os.path.splitext(os.path.basename(sys.argv[0]))[0]
default_logfile_name = '%s-%s-%s.log' % (
def_log_basename,
pydt.datetime.now().strftime('%Y_%m_%d-%H_%M_%S'),
os.getpid()
)
# given on command line ?
for option in sys.argv[1:]:
if option.startswith('--log-file='):
(opt_name, value) = option.split('=')
(dir_name, file_name) = os.path.split(value)
if dir_name == '':
dir_name = '.'
if file_name == '':
file_name = default_logfile_name
_logfile_name = os.path.abspath(os.path.expanduser(os.path.join(dir_name, file_name)))
return True
# else store it in ~/.local/gnumed/logs/def_log_basename/default_logfile_name
dir_name = os.path.expanduser(os.path.join('~', '.local', 'gnumed', 'logs', def_log_basename))
try:
os.makedirs(dir_name)
except OSError as e:
if (e.errno == 17) and not os.path.isdir(dir_name):
raise
_logfile_name = os.path.join(dir_name, default_logfile_name)
return True
#===============================================================
# main
#---------------------------------------------------------------
__setup_logging()
if __name__ == '__main__':
if len(sys.argv) < 2:
sys.exit()
if sys.argv[1] != 'test':
sys.exit()
print_logfile_name()
#-----------------------------------------------------------
def test():
print_logfile_name()
log_step(message = 'testing')
logger = logging.getLogger('gmLog2.test')
log_step()
logger.error('test %s', [1,2,3])
log_step(message = 'still testing')
logger.error("I expected to see %s::test()" % __file__)
log_step(message = 'restarting', restart = True)
add_word2hide('super secret passphrase')
log_step()
logger.debug('credentials: super secret passphrase')
log_step()
try:
int(None)
except Exception:
logger.exception('unhandled exception')
log_stack_trace()
flush()
log_step(message = 'done')
#-----------------------------------------------------------
test()
Functions
def add_word2hide(word: str)
-
Add a string to the list of strings to be scrubbed from logging output.
Useful for hiding credentials etc.
Expand source code
def add_word2hide(word:str): """Add a string to the list of strings to be scrubbed from logging output. Useful for hiding credentials etc. """ if word is None: return if word.strip() == '': return if word not in __words2hide: __words2hide.append(str(word))
def flush()
-
Log a
line and flush handlers. Expand source code
def flush(): """Log a <synced> line and flush handlers.""" logger = logging.getLogger('gm.logging') logger.critical('-------- synced log file -------------------------------') root_logger = logging.getLogger() for handler in root_logger.handlers: handler.flush()
def log_instance_state(instance)
-
Log the state of a class instance.
Expand source code
def log_instance_state(instance): """Log the state of a class instance.""" logger = logging.getLogger('gm.logging') logger.debug('state of %s', instance) for attr in [ a for a in dir(instance) if not a.startswith('__') ]: try: val = getattr(instance, attr) except AttributeError: val = '<cannot access>' logger.debug(' %s: %s', attr, val)
def log_multiline(level, message: str = None, line_prefix: str = None, text: str = None)
-
Log multi-line text in a standard format.
Args
level
- a log level
message
- an arbitrary message to add in
line_prefix
- a string to prefix lines with
text
- the multi-line text to log
Expand source code
def log_multiline(level, message:str=None, line_prefix:str=None, text:str=None): """Log multi-line text in a standard format. Args: level: a log level message: an arbitrary message to add in line_prefix: a string to prefix lines with text: the multi-line text to log """ if text is None: return if message is None: message = 'multiline text:' if line_prefix is None: line_template = ' > %s' else: line_template = '%s: %%s' % line_prefix lines2log = [message] lines2log.extend([ line_template % line for line in text.split('\n') ]) logger = logging.getLogger('gm.logging') logger.log(level, '\n'.join(lines2log))
def log_stack_trace(message: str = None, t=None, v=None, tb=None)
-
Log exception details and stack trace.
(t,v,tb) are what sys.exc_info() returns.
If any of (t,v,tb) is None it is attempted to be retrieved from sys.exc_info().
Args
message
- arbitrary message to add in
t
- an exception type
v
- an exception value
tb
- a traceback object
Expand source code
def log_stack_trace(message:str=None, t=None, v=None, tb=None): """Log exception details and stack trace. (t,v,tb) are what sys.exc_info() returns. If any of (t,v,tb) is None it is attempted to be retrieved from sys.exc_info(). Args: message: arbitrary message to add in t: an exception type v: an exception value tb: a traceback object """ logger = logging.getLogger('gm.logging') if t is None: t = sys.exc_info()[0] if v is None: v = sys.exc_info()[1] if tb is None: tb = sys.exc_info()[2] if tb is None: logger.debug('sys.exc_info() did not return a traceback object, trying sys.last_traceback') try: tb = sys.last_traceback except AttributeError: logger.debug('no stack to trace (no exception information available)') return # log exception details logger.debug('exception: %s', v) logger.debug('type: %s', t) logger.debug('list of attributes:') for attr in [ a for a in dir(v) if not a.startswith('__') ]: try: val = getattr(v, attr) except AttributeError: val = '<cannot access>' logger.debug(' %s: %s', attr, val) # make sure we don't leave behind a binding # to the traceback as warned against in # sys.exc_info() documentation try: # recurse back to root caller while 1: if not tb.tb_next: break tb = tb.tb_next # put the frames on a stack stack_of_frames = [] frame = tb.tb_frame while frame: stack_of_frames.append(frame) frame = frame.f_back finally: del tb stack_of_frames.reverse() if message is not None: logger.debug(message) logger.debug('stack trace follows:') logger.debug('(locals by frame, outmost frame first)') for frame in stack_of_frames: logger.debug ( '--- frame [%s]: #%s, %s -------------------', frame.f_code.co_name, frame.f_lineno, frame.f_code.co_filename ) for varname, value in frame.f_locals.items(): if varname == '__doc__': continue logger.debug('%20s = %s', varname, value)
def log_step(level: int = 10, message: str = None, restart: bool = False)
-
Expand source code
def log_step(level:int=logging.DEBUG, message:str=None, restart:bool=False): if restart: global __current_log_step __current_log_step = 1 logger = logging.getLogger('gm.logging') if message: logger.log(level, '%s - %s', __current_log_step, message) else: logger.log(level, '%s', __current_log_step) __current_log_step += 1
def print_logfile_name()
-
Expand source code
def print_logfile_name(): global _logfile_name_printed if _logfile_name_printed: return print('Log file:', _logfile_name) _logfile_name_printed = True