logger.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. """logging facilities
  2. The way to use this is as follows:
  3. * each module declares its own logger, using:
  4. from .logger import create_logger
  5. logger = create_logger()
  6. * then each module uses logger.info/warning/debug/etc according to the
  7. level it believes is appropriate:
  8. logger.debug('debugging info for developers or power users')
  9. logger.info('normal, informational output')
  10. logger.warning('warn about a non-fatal error or sth else')
  11. logger.error('a fatal error')
  12. ... and so on. see the `logging documentation
  13. <https://docs.python.org/3/howto/logging.html#when-to-use-logging>`_
  14. for more information
  15. * console interaction happens on stderr, that includes interactive
  16. reporting functions like `help`, `info` and `list`
  17. * ...except ``input()`` is special, because we can't control the
  18. stream it is using, unfortunately. we assume that it won't clutter
  19. stdout, because interaction would be broken then anyways
  20. * what is output on INFO level is additionally controlled by commandline
  21. flags
  22. """
  23. import inspect
  24. import logging
  25. import logging.config
  26. import logging.handlers # needed for handlers defined there being configurable in logging.conf file
  27. import os
  28. import warnings
  29. configured = False
  30. # use something like this to ignore warnings:
  31. # warnings.filterwarnings('ignore', r'... regex for warning message to ignore ...')
  32. def _log_warning(message, category, filename, lineno, file=None, line=None):
  33. # for warnings, we just want to use the logging system, not stderr or other files
  34. msg = "{0}:{1}: {2}: {3}".format(filename, lineno, category.__name__, message)
  35. logger = create_logger(__name__)
  36. # Note: the warning will look like coming from here,
  37. # but msg contains info about where it really comes from
  38. logger.warning(msg)
  39. def setup_logging(stream=None, conf_fname=None, env_var='BORG_LOGGING_CONF', level='info', is_serve=False):
  40. """setup logging module according to the arguments provided
  41. if conf_fname is given (or the config file name can be determined via
  42. the env_var, if given): load this logging configuration.
  43. otherwise, set up a stream handler logger on stderr (by default, if no
  44. stream is provided).
  45. if is_serve == True, we configure a special log format as expected by
  46. the borg client log message interceptor.
  47. """
  48. global configured
  49. err_msg = None
  50. if env_var:
  51. conf_fname = os.environ.get(env_var, conf_fname)
  52. if conf_fname:
  53. try:
  54. conf_fname = os.path.abspath(conf_fname)
  55. # we open the conf file here to be able to give a reasonable
  56. # error message in case of failure (if we give the filename to
  57. # fileConfig(), it silently ignores unreadable files and gives
  58. # unhelpful error msgs like "No section: 'formatters'"):
  59. with open(conf_fname) as f:
  60. logging.config.fileConfig(f)
  61. configured = True
  62. logger = logging.getLogger(__name__)
  63. logger.debug('using logging configuration read from "{0}"'.format(conf_fname))
  64. warnings.showwarning = _log_warning
  65. return None
  66. except Exception as err: # XXX be more precise
  67. err_msg = str(err)
  68. # if we did not / not successfully load a logging configuration, fallback to this:
  69. logger = logging.getLogger('')
  70. handler = logging.StreamHandler(stream)
  71. if is_serve:
  72. fmt = '$LOG %(levelname)s Remote: %(message)s'
  73. else:
  74. fmt = '%(message)s'
  75. handler.setFormatter(logging.Formatter(fmt))
  76. logger.addHandler(handler)
  77. logger.setLevel(level.upper())
  78. configured = True
  79. logger = logging.getLogger(__name__)
  80. if err_msg:
  81. logger.warning('setup_logging for "{0}" failed with "{1}".'.format(conf_fname, err_msg))
  82. logger.debug('using builtin fallback logging configuration')
  83. warnings.showwarning = _log_warning
  84. return handler
  85. def find_parent_module():
  86. """find the name of a the first module calling this module
  87. if we cannot find it, we return the current module's name
  88. (__name__) instead.
  89. """
  90. try:
  91. frame = inspect.currentframe().f_back
  92. module = inspect.getmodule(frame)
  93. while module is None or module.__name__ == __name__:
  94. frame = frame.f_back
  95. module = inspect.getmodule(frame)
  96. return module.__name__
  97. except AttributeError:
  98. # somehow we failed to find our module
  99. # return the logger module name by default
  100. return __name__
  101. def create_logger(name=None):
  102. """lazily create a Logger object with the proper path, which is returned by
  103. find_parent_module() by default, or is provided via the commandline
  104. this is really a shortcut for:
  105. logger = logging.getLogger(__name__)
  106. we use it to avoid errors and provide a more standard API.
  107. We must create the logger lazily, because this is usually called from
  108. module level (and thus executed at import time - BEFORE setup_logging()
  109. was called). By doing it lazily we can do the setup first, we just have to
  110. be careful not to call any logger methods before the setup_logging() call.
  111. If you try, you'll get an exception.
  112. """
  113. class LazyLogger:
  114. def __init__(self, name=None):
  115. self.__name = name or find_parent_module()
  116. self.__real_logger = None
  117. @property
  118. def __logger(self):
  119. if self.__real_logger is None:
  120. if not configured:
  121. raise Exception("tried to call a logger before setup_logging() was called")
  122. self.__real_logger = logging.getLogger(self.__name)
  123. return self.__real_logger
  124. def setLevel(self, *args, **kw):
  125. return self.__logger.setLevel(*args, **kw)
  126. def log(self, *args, **kw):
  127. return self.__logger.log(*args, **kw)
  128. def exception(self, *args, **kw):
  129. return self.__logger.exception(*args, **kw)
  130. def debug(self, *args, **kw):
  131. return self.__logger.debug(*args, **kw)
  132. def info(self, *args, **kw):
  133. return self.__logger.info(*args, **kw)
  134. def warning(self, *args, **kw):
  135. return self.__logger.warning(*args, **kw)
  136. def error(self, *args, **kw):
  137. return self.__logger.error(*args, **kw)
  138. def critical(self, *args, **kw):
  139. return self.__logger.critical(*args, **kw)
  140. return LazyLogger(name)