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