/behave/log_capture.py
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])