PageRenderTime 53ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/behave/log_capture.py

https://github.com/atteroTheGreatest/behave
Python | 232 lines | 143 code | 26 blank | 63 comment | 28 complexity | 8f134e99a84564a8f4dbb797bcdaed0e MD5 | raw file
  1. import logging
  2. import functools
  3. from logging.handlers import BufferingHandler
  4. import re
  5. from behave.configuration import ConfigError
  6. class RecordFilter(object):
  7. '''Implement logging record filtering as per the configuration
  8. --logging-filter option.
  9. '''
  10. def __init__(self, names):
  11. self.include = set()
  12. self.exclude = set()
  13. for name in names.split(','):
  14. if name[0] == '-':
  15. self.exclude.add(name[1:])
  16. else:
  17. self.include.add(name)
  18. def filter(self, record):
  19. if self.exclude:
  20. return record.name not in self.exclude
  21. return record.name in self.include
  22. # originally from nostetsts logcapture plugin
  23. class LoggingCapture(BufferingHandler):
  24. '''Capture logging events in a memory buffer for later display or query.
  25. Captured logging events are stored on the attribute
  26. :attr:`~LoggingCapture.buffer`:
  27. .. attribute:: buffer
  28. This is a list of captured logging events as `logging.LogRecords`_.
  29. .. _`logging.LogRecords`:
  30. http://docs.python.org/library/logging.html#logrecord-objects
  31. By default the format of the messages will be::
  32. '%(levelname)s:%(name)s:%(message)s'
  33. This may be overridden using standard logging formatter names in the
  34. configuration variable ``logging_format``.
  35. The level of logging captured is set to ``logging.NOTSET`` by default. You
  36. may override this using the configuration setting ``logging_level`` (which
  37. is set to a level name.)
  38. Finally there may be `filtering of logging events`__ specified by the
  39. configuration variable ``logging_filter``.
  40. .. __: behave.html#command-line-arguments
  41. '''
  42. def __init__(self, config, level=None):
  43. BufferingHandler.__init__(self, 1000)
  44. self.config = config
  45. self.old_handlers = []
  46. self.old_level = None
  47. # set my formatter
  48. fmt = datefmt = None
  49. if config.logging_format:
  50. fmt = config.logging_format
  51. else:
  52. fmt = '%(levelname)s:%(name)s:%(message)s'
  53. if config.logging_datefmt:
  54. datefmt = config.logging_datefmt
  55. fmt = logging.Formatter(fmt, datefmt)
  56. self.setFormatter(fmt)
  57. # figure the level we're logging at
  58. if level is not None:
  59. self.level = level
  60. elif config.logging_level:
  61. self.level = config.logging_level
  62. else:
  63. self.level = logging.NOTSET
  64. # construct my filter
  65. if config.logging_filter:
  66. self.addFilter(RecordFilter(config.logging_filter))
  67. def __nonzero__(self):
  68. return bool(self.buffer)
  69. def flush(self):
  70. pass # do nothing
  71. def truncate(self):
  72. self.buffer = []
  73. def getvalue(self):
  74. return '\n'.join(self.formatter.format(r) for r in self.buffer)
  75. def findEvent(self, pattern):
  76. '''Search through the buffer for a message that matches the given
  77. regular expression.
  78. Returns boolean indicating whether a match was found.
  79. '''
  80. pattern = re.compile(pattern)
  81. for record in self.buffer:
  82. if pattern.search(record.getMessage()) is not None:
  83. return True
  84. return False
  85. def any_errors(self):
  86. '''Search through the buffer for any ERROR or CRITICAL events.
  87. Returns boolean indicating whether a match was found.
  88. '''
  89. return any(record for record in self.buffer
  90. if record.levelname in ('ERROR', 'CRITICAL'))
  91. def inveigle(self):
  92. '''Turn on logging capture by replacing all existing handlers
  93. configured in the logging module.
  94. If the config var logging_clear_handlers is set then we also remove
  95. all existing handlers.
  96. We also set the level of the root logger.
  97. The opposite of this is :meth:`~LoggingCapture.abandon`.
  98. '''
  99. root_logger = logging.getLogger()
  100. if self.config.logging_clear_handlers:
  101. # kill off all the other log handlers
  102. for logger in logging.Logger.manager.loggerDict.values():
  103. if hasattr(logger, "handlers"):
  104. for handler in logger.handlers:
  105. self.old_handlers.append((logger, handler))
  106. logger.removeHandler(handler)
  107. # sanity check: remove any existing LoggingCapture
  108. for handler in root_logger.handlers[:]:
  109. if isinstance(handler, LoggingCapture):
  110. root_logger.handlers.remove(handler)
  111. elif self.config.logging_clear_handlers:
  112. self.old_handlers.append((root_logger, handler))
  113. root_logger.removeHandler(handler)
  114. # right, we're it now
  115. root_logger.addHandler(self)
  116. # capture the level we're interested in
  117. self.old_level = root_logger.level
  118. root_logger.setLevel(self.level)
  119. def abandon(self):
  120. '''Turn off logging capture.
  121. If other handlers were removed by :meth:`~LoggingCapture.inveigle` then
  122. they are reinstated.
  123. '''
  124. root_logger = logging.getLogger()
  125. for handler in root_logger.handlers[:]:
  126. if handler is self:
  127. root_logger.handlers.remove(handler)
  128. if self.config.logging_clear_handlers:
  129. for logger, handler in self.old_handlers:
  130. logger.addHandler(handler)
  131. if self.old_level is not None:
  132. # -- RESTORE: Old log.level before inveigle() was used.
  133. root_logger.setLevel(self.old_level)
  134. self.old_level = None
  135. # pre-1.2 backwards compatibility
  136. MemoryHandler = LoggingCapture
  137. def capture(*args, **kw):
  138. '''Decorator to wrap an *environment file function* in log file capture.
  139. It configures the logging capture using the *behave* context - the first
  140. argument to the function being decorated (so don't use this to decorate
  141. something that doesn't have *context* as the first argument.)
  142. The basic usage is:
  143. .. code-block: python
  144. @capture
  145. def after_scenario(context, scenario):
  146. ...
  147. The function prints any captured logging (at the level determined by the
  148. ``log_level`` configuration setting) directly to stdout, regardless of
  149. error conditions.
  150. It is mostly useful for debugging in situations where you are seeing a
  151. message like::
  152. No handlers could be found for logger "name"
  153. The decorator takes an optional "level" keyword argument which limits the
  154. level of logging captured, overriding the level in the run's configuration:
  155. .. code-block: python
  156. @capture(level=logging.ERROR)
  157. def after_scenario(context, scenario):
  158. ...
  159. This would limit the logging captured to just ERROR and above, and thus
  160. only display logged events if they are interesting.
  161. '''
  162. def create_decorator(func, level=None):
  163. def f(context, *args):
  164. h = LoggingCapture(context.config, level=level)
  165. h.inveigle()
  166. try:
  167. func(context, *args)
  168. finally:
  169. h.abandon()
  170. v = h.getvalue()
  171. if v:
  172. print 'Captured Logging:'
  173. print v
  174. return f
  175. if not args:
  176. return functools.partial(create_decorator, level=kw.get('level'))
  177. else:
  178. return create_decorator(args[0])