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