Gnumed.pycommon.gmLog2

Package Gnumed :: Package pycommon :: Module gmLog2 |
[frames] | no frames]

# Source Code for Module Gnumed.pycommon.gmLog2

      1  """GNUmed logging framework setup.
      2
      3  All error logging, user notification and otherwise unhandled
      4  exception handling should go through classes or functions of
      5  this module.
      6
      7  Theory of operation:
      8
      9  This module tailors the standard logging framework to
     10  the needs of GNUmed.
     11
     12  By importing gmLog2 into your code you'll get the root
     13  logger send to a unicode file with messages in a format useful
     14  for debugging. The filename is either taken from the
     15  command line (--log-file=...) or derived from the name
     16  of the main application.
     17
     18  The log file will be found in one of the following standard
     19  locations:
     20
     21  1) given on the command line as "--log-file=LOGFILE"
     22  2) ~/.<base_name>/<base_name>.log
     23  3) /dir/of/binary/<base_name>.log               (mainly for DOS/Windows)
     24
     25  where <base_name> is derived from the name
     26  of the main application.
     27
     28  If you want to specify just a directory for the log file you
     29  must end the --log-file definition with a slash.
     30
     31  By importing "logging" and getting a logger your modules
     32  never need to worry about the real message destination or whether
     33  at any given time there's a valid logger available.
     34
     35  Your MAIN module simply imports gmLog2 and all other modules
     36  will merrily and automagically start logging away.
     37
     38
     39  Ad hoc call stack logging recipe:
     40
     41          call_stack = inspect.stack()
     42          call_stack.reverse()
     43          for idx in range(1, len(call_stack)):
     44                  caller = call_stack[idx]
     45                  _log.debug('%s[%s] @ [%s] in [%s]', ' '* idx, caller[3], caller[2], caller[1])
     46          del call_stack
     47  """
     48  # TODO:
     49  # - exception()
     50  # - ascii_ctrl2mnemonic()
     51  #========================================================================
     52  __author__  = "K. Hilbert <Karsten.Hilbert@gmx.net>"
     53  __license__ = "GPL v2 or later (details at http://www.gnu.org)"
     54
     55
     56  # stdlib
     57  import logging
     58  import sys
     59  import os
     60  import io
     61  import codecs
     62  import locale
     63  import datetime as pydt
     64  import random
     65  import time
     66  import calendar
     67
     68
     69  _logfile_name = None
     70  _logfile = None
     71
     72  #_string_encoding = None
     73
     74  # table used for cooking non-printables
     75  AsciiName = ['<#0-0x00-nul>',
     76                           '<#1-0x01-soh>',
     77                           '<#2-0x02-stx>',
     78                           '<#3-0x03-etx>',
     79                           '<#4-0x04-eot>',
     80                           '<#5-0x05-enq>',
     81                           '<#6-0x06-ack>',
     82                           '<#7-0x07-bel>',
     83                           '<#8-0x08-bs>',
     84                           '<#9-0x09-ht>',
     85                           '<#10-0x0A-lf>',
     86                           '<#11-0x0B-vt>',
     87                           '<#12-0x0C-ff>',
     88                           '<#13-0x0D-cr>',
     89                           '<#14-0x0E-so>',
     90                           '<#15-0x0F-si>',
     91                           '<#16-0x10-dle>',
     92                           '<#17-0x11-dc1/xon>',
     93                           '<#18-0x12-dc2>',
     94                           '<#19-0x13-dc3/xoff>',
     95                           '<#20-0x14-dc4>',
     96                           '<#21-0x15-nak>',
     97                           '<#22-0x16-syn>',
     98                           '<#23-0x17-etb>',
     99                           '<#24-0x18-can>',
    100                           '<#25-0x19-em>',
    101                           '<#26-0x1A-sub>',
    102                           '<#27-0x1B-esc>',
    103                           '<#28-0x1C-fs>',
    104                           '<#29-0x1D-gs>',
    105                           '<#30-0x1E-rs>',
    106                           '<#31-0x1F-us>'
    107                          ]
    108
    109  # msg = reduce(lambda x, y: x+y, (map(self.__char2AsciiName, list(tmp))), '')
    110  #
    111  #       def __char2AsciiName(self, aChar):
    112  #               try:
    113  #                       return AsciiName[ord(aChar)]
    114  #               except IndexError:
    115  #                       return aChar
    116  #
    117  #       def __tracestack(self):
    118  #               """extract data from the current execution stack
    119  #
    120  #               this is rather fragile, I guess
    121  #               """
    122  #               stack = traceback.extract_stack()
    123  #               self.__modulename = stack[-4][0]
    124  #               self.__linenumber = stack[-4][1]
    125  #               self.__functionname = stack[-4][2]
    126  #               if (self.__functionname == "?"):
    127  #                       self.__functionname = "Main"
    128
    129  #===============================================================
    130  # external API
    131  #===============================================================
    132 -def flush():
    133          logger = logging.getLogger('gm.logging')
    134          logger.critical('-------- synced log file -------------------------------')
    135          root_logger = logging.getLogger()
    136          for handler in root_logger.handlers:
    137                  handler.flush()
    138
    139  #===============================================================
    140 -def log_instance_state(instance):
    141          logger = logging.getLogger('gm.logging')
    142          logger.debug('state of %s', instance)
    143          for attr in [ a for a in dir(instance) if not a.startswith('__') ]:
    144                  logger.debug('  %s: %s', attr, getattr(instance, attr))
    145
    146  #===============================================================
    147 -def log_stack_trace(message=None, t=None, v=None, tb=None):
    148
    149          logger = logging.getLogger('gm.logging')
    150
    151          if t is None:
    152                  t = sys.exc_info()[0]
    153          if v is None:
    154                  v = sys.exc_info()[1]
    155          if tb is None:
    156                  tb = sys.exc_info()[2]
    157          if tb is None:
    158                  logger.debug('sys.exc_info() did not return a traceback object, trying sys.last_traceback')
    159                  try:
    160                          tb = sys.last_traceback
    161                  except AttributeError:
    162                          logger.debug('no stack to trace (no exception information available)')
    163                          return
    164
    165          # log exception details
    166          logger.debug('exception: %s', v)
    167          logger.debug('type: %s', t)
    168          logger.debug('list of attributes:')
    169          for attr in [ a for a in dir(v) if not a.startswith('__') ]:
    170                  logger.debug('  %s: %s', attr, getattr(v, attr))
    171
    172          # make sure we don't leave behind a binding
    173          # to the traceback as warned against in
    174          # sys.exc_info() documentation
    175          try:
    176                  # recurse back to root caller
    177                  while 1:
    178                          if not tb.tb_next:
    179                                  break
    180                          tb = tb.tb_next
    181                  # put the frames on a stack
    182                  stack_of_frames = []
    183                  frame = tb.tb_frame
    184                  while frame:
    185                          stack_of_frames.append(frame)
    186                          frame = frame.f_back
    187          finally:
    188                  del tb
    189          stack_of_frames.reverse()
    190
    191          if message is not None:
    192                  logger.debug(message)
    193          logger.debug('stack trace follows:')
    194          logger.debug('(locals by frame, outmost frame first)')
    195          for frame in stack_of_frames:
    196                  logger.debug (
    197                          '--- frame [%s]: #%s, %s -------------------',
    198                          frame.f_code.co_name,
    199                          frame.f_lineno,
    200                          frame.f_code.co_filename
    201                  )
    202                  for varname, value in frame.f_locals.items():
    203                          if varname == '__doc__':
    204                                  continue
    205                          logger.debug('%20s = %s', varname, value)
    206
    207  #---------------------------------------------------------------
    208 -def log_multiline(level, message=None, line_prefix=None, text=None):
    209          if text is None:
    210                  return
    211          if message is None:
    212                  message = 'multiline text:'
    213          if line_prefix is None:
    214                  line_template = '  > %s'
    215          else:
    216                  line_template = '%s: %%s' % line_prefix
    217          lines2log = [message]
    218          lines2log.extend([ line_template % line for line in text.split('\n') ])
    219          logger = logging.getLogger('gm.logging')
    220          logger.log(level, '\n'.join(lines2log))
    221
    222  #===============================================================
    223  # internal API
    224  #===============================================================
    225  __words2hide = []
    226
    227 -def add_word2hide(word):
    228          if word is None:
    229                  return
    230          if word.strip() == '':
    231                  return
    232          if word not in __words2hide:
    233                  __words2hide.append(str(word))
    234
    235  #---------------------------------------------------------------
    236  __original_logger_write_func = None
    237
    238 -def __safe_logger_write_func(s):
    239          for word in __words2hide:
    240                  # throw away up to 4 bits (plus the randint() cost)
    241                  random.getrandbits(random.randint(1, 4))
    242                  # from there generate a replacement string valid for
    243                  # *this* round of replacements of *this* word,
    244                  # this approach won't mitigate guessing trivial passwords
    245                  # from replacements of known data (a known-plaintext attack)
    246                  # but will make automated searching for replaced strings
    247                  # in the log more difficult
    248                  bummer = hex(random.randint(0, sys.maxsize)).lstrip('0x')
    249                  s = s.replace(word, bummer)
    250          __original_logger_write_func(s)
    251
    252  #---------------------------------------------------------------
    253 -def __setup_logging():
    254
    255          global _logfile
    256          if _logfile is not None:
    257                  return True
    258
    259          if not __get_logfile_name():
    260                  return False
    261
    262          _logfile = io.open(_logfile_name, mode = 'wt', encoding = 'utf8', errors = 'replace')
    263          global __original_logger_write_func
    264          __original_logger_write_func = _logfile.write
    265          _logfile.write = __safe_logger_write_func
    266
    267          # setup
    268          fmt = '%(asctime)s  %(levelname)-8s  %(name)-12s  [%(thread)d %(threadName)-10s]  (%(pathname)s::%(funcName)s() #%(lineno)d): %(message)s'
    269          logging.basicConfig (
    270                  format = fmt,
    271                  datefmt = '%Y-%m-%d %H:%M:%S',
    272                  level = logging.DEBUG,
    273                  stream = _logfile
    274          )
    275          logging.captureWarnings(True)
    276          logger = logging.getLogger()
    277          logger.log_stack_trace = log_stack_trace
    278          logger.log_multiline = log_multiline
    279
    280          # start logging
    281          #logger = logging.getLogger('gm.logging')
    282          logger.critical('-------- start of logging ------------------------------')
    283          logger.info('log file is <%s>', _logfile_name)
    284          logger.info('log level is [%s]', logging.getLevelName(logger.getEffectiveLevel()))
    285          logger.info('log file encoding is <utf8>')
    286          logger.debug('log file .write() patched from original %s to patched %s', __original_logger_write_func, __safe_logger_write_func)
    287
    288  #---------------------------------------------------------------
    289 -def __get_logfile_name():
    290
    291          global _logfile_name
    292          if _logfile_name is not None:
    293                  return _logfile_name
    294
    295          def_log_basename = os.path.splitext(os.path.basename(sys.argv[0]))[0]
    296          default_logfile_name = '%s-%s-%s.log' % (
    297                  def_log_basename,
    298                  pydt.datetime.now().strftime('%Y_%m_%d-%H_%M_%S'),
    299                  os.getpid()
    300          )
    301
    302          # given on command line ?
    303          for option in sys.argv[1:]:
    304                  if option.startswith('--log-file='):
    305                          (opt_name, value) = option.split('=')
    306                          (dir_name, file_name) = os.path.split(value)
    307                          if dir_name == '':
    308                                  dir_name = '.'
    309                          if file_name == '':
    310                                  file_name = default_logfile_name
    311                          _logfile_name = os.path.abspath(os.path.expanduser(os.path.join(dir_name, file_name)))
    312                          return True
    313
    314          # else store it in ~/.gnumed/logs/def_log_basename/default_logfile_name
    315          dir_name = os.path.expanduser(os.path.join('~', '.gnumed', 'logs', def_log_basename))
    316          try:
    317                  os.makedirs(dir_name)
    318          except OSError as e:
    319                  if (e.errno == 17) and not os.path.isdir(dir_name):
    320                          raise
    321
    322          _logfile_name = os.path.join(dir_name, default_logfile_name)
    323
    324          return True
    325
    326  #===============================================================
    327  # main
    328  #---------------------------------------------------------------
    329  __setup_logging()
    330
    331  if __name__ == '__main__':
    332
    333          if len(sys.argv) < 2:
    334                  sys.exit()
    335
    336          if sys.argv[1] != 'test':
    337                  sys.exit()
    338
    339          #-----------------------------------------------------------
    340 -        def test():
    341                  logger = logging.getLogger('gmLog2.test')
    342                  logger.error("I expected to see %s::test()" % __file__)
    343                  add_word2hide('super secret passphrase')
    344                  logger.debug('credentials: super secret passphrase')
    345
    346                  try:
    347                          int(None)
    348                  except Exception:
    349                          logger.exception('unhandled exception')
    350                          log_stack_trace()
    351                  flush()
    352          #-----------------------------------------------------------
    353          test()
    354