backlash 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ from backlash._compat import string_types, bytes_
2
+ from backlash.tbtools import get_current_traceback
3
+ from backlash.utils import RequestContext
4
+
5
+ import logging
6
+ log = logging.getLogger('backlash')
7
+
8
+
9
+ class TraceErrorsMiddleware(object):
10
+ def __init__(self, application, reporters, context_injectors):
11
+ self.app = application
12
+ self.reporters = reporters
13
+ self.context_injectors = context_injectors
14
+
15
+ def _report_errors(self, environ, recorded_exc_info=None):
16
+ context = RequestContext({'environ': dict(environ)})
17
+ for injector in self.context_injectors:
18
+ context.update(injector(environ))
19
+
20
+ traceback = get_current_traceback(skip=2, show_hidden_frames=False, context=context,
21
+ exc_info=recorded_exc_info)
22
+
23
+ # This will lead to double logging in case backlash logger is set to DEBUG
24
+ # but this is actually wanted as some environments, like WebTest, swallow
25
+ # wsgi.environ making the traceback totally disappear.
26
+ log.debug(traceback.plaintext)
27
+ traceback.log(environ['wsgi.errors'])
28
+
29
+ for r in self.reporters:
30
+ try:
31
+ r.report(traceback)
32
+ except Exception:
33
+ error = get_current_traceback(skip=1, show_hidden_frames=False)
34
+ environ['wsgi.errors'].write('\nError while reporting exception with %s\n' % r)
35
+ environ['wsgi.errors'].write(error.plaintext)
36
+
37
+ def _generate_response(self, environ, start_response):
38
+ try:
39
+ start_response('500 INTERNAL SERVER ERROR', [
40
+ ('Content-Type', 'text/html; charset=utf-8'),
41
+ # Disable Chrome's XSS protection, the debug
42
+ # output can cause false-positives.
43
+ ('X-XSS-Protection', '0'),
44
+ ])
45
+ except Exception:
46
+ # if we end up here there has been output but an error
47
+ # occurred. in that situation we can do nothing fancy any
48
+ # more, better log something into the error log and fall
49
+ # back gracefully.
50
+ environ['wsgi.errors'].write(
51
+ 'Debugging middleware caught exception in streamed '
52
+ 'response at a point where response headers were already '
53
+ 'sent.\n')
54
+ else:
55
+ yield bytes_('Internal Server Error')
56
+
57
+ def _report_errors_with_response(self, environ, start_response):
58
+ self._report_errors(environ, None)
59
+ return self._generate_response(environ, start_response)
60
+
61
+ def _report_errors_while_consuming_iter(self, app_iter, environ, start_response):
62
+ try:
63
+ try:
64
+ for item in app_iter:
65
+ yield item
66
+ finally: # also after _report_errors_while_consuming_iter().close() injected GeneratorExit
67
+ if hasattr(app_iter, 'close'):
68
+ app_iter.close()
69
+ except Exception:
70
+ for chunk in self._report_errors_with_response(environ, start_response):
71
+ yield chunk
72
+
73
+ def __call__(self, environ, start_response):
74
+ app_iter = None
75
+ try:
76
+ app_iter = self.app(environ, start_response)
77
+ except Exception:
78
+ return list(self._report_errors_with_response(environ, start_response))
79
+ else:
80
+ recorded_exc_info = environ.pop('backlash.exc_info', None)
81
+ if recorded_exc_info is not None:
82
+ environ = environ.pop('backlash.exc_environ', environ)
83
+ self._report_errors(environ, recorded_exc_info)
84
+
85
+ return self._report_errors_while_consuming_iter(app_iter, environ, start_response)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,126 @@
1
+ from email.mime.application import MIMEApplication
2
+ from email.mime.multipart import MIMEMultipart
3
+ from email.mime.text import MIMEText
4
+ import smtplib
5
+ import ssl
6
+ from backlash._compat import string_types, bytes_, text_, PY3
7
+
8
+
9
+ class EmailReporter(object):
10
+ def __init__(self, smtp_server=None, from_address=None, error_email=None,
11
+ smtp_username=None, smtp_password=None, smtp_use_tls=False,
12
+ error_subject_prefix='', dump_request=False, dump_request_size=50000,
13
+ dump_local_frames=False, dump_local_frames_count=2,
14
+ smtp_port=None, **unused):
15
+ self.smtp_server = smtp_server
16
+ self.smtp_port = smtp_port
17
+ self.from_address = from_address
18
+
19
+ self.smtp_username = smtp_username
20
+ self.smtp_password = smtp_password
21
+
22
+ self.smtp_use_tls = smtp_use_tls
23
+
24
+ if isinstance(error_email, string_types):
25
+ error_email = error_email.split(",")
26
+ self.error_email = error_email
27
+
28
+ self.error_subject_prefix = error_subject_prefix
29
+ self.dump_request = dump_request
30
+ self.dump_request_size = dump_request_size
31
+ self.dump_local_frames = dump_local_frames
32
+ self.dump_local_frames_count = dump_local_frames_count
33
+
34
+ if not self.smtp_server or not self.from_address or not self.error_email:
35
+ raise ValueError("Backlash email reporting requires "
36
+ "smtp_server, from_address and error_email settings")
37
+
38
+ def report(self, traceback):
39
+ msg = self.assemble_email(traceback)
40
+
41
+ server = smtplib.SMTP(self.smtp_server, self.smtp_port)
42
+ if self.smtp_use_tls:
43
+ server.ehlo()
44
+ server.starttls()
45
+ server.ehlo()
46
+
47
+ if self.smtp_username and self.smtp_password:
48
+ server.login(self.smtp_username, self.smtp_password)
49
+
50
+ result = server.sendmail(self.from_address, self.error_email, msg.as_string())
51
+
52
+ try:
53
+ server.quit()
54
+ except ssl.SSLError:
55
+ # SSLError is raised in tls connections on closing sometimes
56
+ pass
57
+
58
+ def _repr_value(self, value):
59
+ try:
60
+ value = repr(value)
61
+ except Exception as e:
62
+ # Cope with WebOb response objects that might be unable to get their repr
63
+ value = repr(e)
64
+
65
+ if not PY3:
66
+ # On PY2 some repr might actually return bytes
67
+ # In that case just escape them.
68
+ value = value.decode('ascii', 'replace')
69
+
70
+ return value
71
+
72
+ def _format_cgi(self, environ):
73
+ return '\n'.join(
74
+ ('\t%s: %s' % (k, self._repr_value(environ[k])) for k in sorted(environ.keys()) if k.upper() == k))
75
+
76
+ def _format_wsgi(self, environ):
77
+ return '\n'.join(
78
+ ('\t%s: %s' % (k, self._repr_value(environ[k])) for k in sorted(environ.keys()) if k.upper() != k))
79
+
80
+ def email_body(self, traceback):
81
+ body = 'TRACEBACK:\n%s' % traceback.plaintext
82
+
83
+ if self.dump_local_frames:
84
+ body += '\n\n\nLAST FRAMES:'
85
+ frames_base_index = len(traceback.frames) - self.dump_local_frames_count
86
+ for idx, frame in enumerate(traceback.frames[-self.dump_local_frames_count:]):
87
+ body += '\n\tFRAME #%d\n' % (frames_base_index+idx)
88
+ for key, value in frame.locals.items():
89
+ body += "\t\t%20s = " % key
90
+ try:
91
+ body += '%r\n' % (value,)
92
+ except Exception as e:
93
+ body += "<UNABLE TO PRINT VALUE>\n"
94
+
95
+ body += '\n\n\nENVIRON:\n%s' % self._format_cgi(traceback.context['environ'])
96
+ body += '\n\n\nWSGI:\n%s' % self._format_wsgi(traceback.context['environ'])
97
+
98
+ for entry, value in traceback.context.items():
99
+ if entry == 'environ':
100
+ continue
101
+
102
+ body += '\n\n\n%s:\n\t%r' % (entry.upper(), value)
103
+
104
+ return body
105
+
106
+ def assemble_email(self, traceback):
107
+ msg = MIMEMultipart()
108
+
109
+ subject = text_(traceback.exception)
110
+
111
+ msg['Subject'] = text_(self.error_subject_prefix + subject).replace('\n', ' ')
112
+ msg['From'] = text_(self.from_address)
113
+ msg['To'] = text_(', '.join(self.error_email))
114
+
115
+ text = MIMEText(bytes_(self.email_body(traceback)), 'plain', 'utf-8')
116
+ text.set_type('text/plain')
117
+ text.set_param('charset', 'UTF-8')
118
+ msg.attach(text)
119
+
120
+ request = traceback.context.get('request')
121
+ if self.dump_request and request is not None:
122
+ part = MIMEApplication(request.as_bytes(self.dump_request_size))
123
+ part.add_header('Content-Disposition', 'attachment; filename="request.txt"')
124
+ msg.attach(part)
125
+
126
+ return msg
@@ -0,0 +1,40 @@
1
+ try:
2
+ from raven.base import Client
3
+ from raven.utils.wsgi import get_current_url, get_headers, get_environ
4
+ except ImportError:
5
+ Client = None
6
+
7
+
8
+ class SentryReporter(object):
9
+ def __init__(self, sentry_dsn, **unused):
10
+ if Client is None:
11
+ raise RavenNotAvailable('Raven is not installed, maybe run "pip install raven"')
12
+
13
+ self.client = Client(sentry_dsn)
14
+
15
+ def report(self, traceback):
16
+ environ = traceback.context.get('environ', {})
17
+ data = {
18
+ 'sentry.interfaces.Http': {
19
+ 'method': environ.get('REQUEST_METHOD'),
20
+ 'url': get_current_url(environ, strip_querystring=True),
21
+ 'query_string': environ.get('QUERY_STRING'),
22
+ # TODO
23
+ # 'data': environ.get('wsgi.input'),
24
+ 'headers': dict(get_headers(environ)),
25
+ 'env': dict(get_environ(environ)),
26
+ }
27
+ }
28
+
29
+ is_backlash_event = getattr(traceback.exc_value, 'backlash_event', False)
30
+ if is_backlash_event:
31
+ # Just a Stack Dump request from backlash
32
+ self.client.captureMessage(traceback.exception, data=data,
33
+ stack=traceback.frames)
34
+ else:
35
+ # This is a real crash
36
+ self.client.captureException(exc_info=traceback.exc_info, data=data)
37
+
38
+
39
+ class RavenNotAvailable(Exception):
40
+ pass
@@ -0,0 +1 @@
1
+ from .middleware import TraceSlowRequestsMiddleware
@@ -0,0 +1,113 @@
1
+ import datetime as dt
2
+ import os
3
+ import threading
4
+ import logging
5
+
6
+
7
+ from backlash.tbtools import get_current_traceback
8
+ from backlash.frtools import get_thread_stack, DumpThread
9
+ from backlash.utils import RequestContext
10
+ from .timer import Timer
11
+
12
+
13
+ class TraceSlowRequestsMiddleware(object):
14
+
15
+ def __init__(self, app, reporters, context_injectors, interval=25,
16
+ exclude_paths=None):
17
+
18
+ self.app = app
19
+ self.reporters = reporters
20
+ self.context_injectors = context_injectors
21
+ self.interval = interval
22
+ self.exclude_paths = exclude_paths or []
23
+
24
+ self.timer = Timer()
25
+ self.timer.daemon = True
26
+ self.timer.start()
27
+
28
+ def _stream_response(self, environ, data):
29
+ try:
30
+ for chunk in data:
31
+ yield chunk
32
+ finally:
33
+ if hasattr(data, 'close'):
34
+ data.close()
35
+ self._cancel_tracing(environ)
36
+
37
+ def __call__(self, environ, start_response):
38
+ try:
39
+ self._start_tracing(environ)
40
+ return self._stream_response(environ, self.app(environ, start_response))
41
+ except Exception:
42
+ self._cancel_tracing(environ)
43
+ raise
44
+
45
+ def peek(self, environ, thread_id, started):
46
+ context = RequestContext({'environ': dict(environ)})
47
+ for injector in self.context_injectors:
48
+ context.update(injector(environ))
49
+
50
+ context.update({
51
+ 'SLOW_REQUEST': {'ThreadID': thread_id,
52
+ 'ProcessID': os.getpid(),
53
+ 'Started': str(started)}
54
+ })
55
+
56
+ try:
57
+ traceback = get_thread_stack(thread_id, environ.get('PATH_INFO', ''),
58
+ context=context, error_type='SlowRequestError')
59
+ except KeyError:
60
+ logging.warn('\nUnable to retrieve SlowRequest Stack %s, '
61
+ 'thread %s probably finished execution in mean time\n',
62
+ environ.get('PATH_INFO', ''), thread_id)
63
+ return
64
+
65
+ for r in self.reporters:
66
+ try:
67
+ r.report(traceback)
68
+ except Exception:
69
+ error = get_current_traceback(skip=1, show_hidden_frames=False)
70
+ environ['wsgi.errors'].write('\nError while reporting slow request with %s\n' % r)
71
+ environ['wsgi.errors'].write(error.plaintext)
72
+
73
+ @classmethod
74
+ def _get_thread_id(cls):
75
+ return threading.current_thread().ident
76
+
77
+ def _is_exempt(self, environ):
78
+ """
79
+ Returns True if this request's URL starts with one of the
80
+ excluded paths.
81
+ """
82
+ exemptions = self.exclude_paths
83
+
84
+ if exemptions:
85
+ path = environ.get('PATH_INFO')
86
+ for excluded_p in self.exclude_paths:
87
+ if path.startswith(excluded_p):
88
+ return True
89
+
90
+ return False
91
+
92
+ def _start_tracing(self, environ):
93
+ if not self._is_exempt(environ):
94
+ job = self.timer.run_later(self.peek,
95
+ self.interval,
96
+ environ,
97
+ self._get_thread_id(),
98
+ dt.datetime.utcnow())
99
+ # In some cases due to webob.Request.call_application() or
100
+ # paste StatusCodeRedirect middleware multiple _start_tracing for the same
101
+ # environ might happen without consuming the app_iter for the firsts
102
+ # and so without canceling them, we register them and cancel them all together.
103
+ environ.setdefault('BACKLASH_SLOW_TRACING_JOBS', []).append(job)
104
+
105
+ def _cancel_tracing(self, environ):
106
+ try:
107
+ tracing_jobs = environ.get('BACKLASH_SLOW_TRACING_JOBS', [])
108
+ for job in tracing_jobs:
109
+ self.timer.cancel(job)
110
+ except Exception:
111
+ error = get_current_traceback(skip=1, show_hidden_frames=False)
112
+ environ['wsgi.errors'].write('Failed to cancel slow requests tracing timer\n')
113
+ environ['wsgi.errors'].write(error.plaintext)
@@ -0,0 +1,107 @@
1
+ from functools import partial
2
+ import logging
3
+ import threading
4
+ import time
5
+
6
+
7
+ class TimerTask(object):
8
+
9
+ def __init__(self, callable_, *args, **kwargs):
10
+ self._callable = partial(callable_, *args, **kwargs)
11
+ self._finished = False
12
+
13
+ def is_finished(self):
14
+ return self._finished
15
+
16
+ def run(self):
17
+ try:
18
+ self._callable()
19
+ except:
20
+ logging.exception('TimerTask failed')
21
+ finally:
22
+ self._finished = True
23
+
24
+
25
+ class Timer(threading.Thread):
26
+ """An alternative to threading.Timer. Where threading.Timer spawns a
27
+ dedicated thread for each job, this class uses a single, long-lived thread
28
+ to process multiple jobs.
29
+
30
+ Jobs are scheduled with a delay value in seconds.
31
+ """
32
+
33
+ def __init__(self, *args, **kwargs):
34
+ super(Timer, self).__init__(*args, **kwargs)
35
+
36
+ self.lock = threading.Condition()
37
+ self._jobs = []
38
+ self.die = False
39
+
40
+ def run_later(self, callable_, timeout, *args, **kwargs):
41
+ """Schedules the specified callable for delayed execution.
42
+
43
+ Returns a TimerTask instance that can be used to cancel pending
44
+ execution.
45
+ """
46
+
47
+ self.lock.acquire()
48
+ try:
49
+ if self.die:
50
+ raise RuntimeError('This timer has been shut down and '
51
+ 'does not accept new jobs.')
52
+
53
+ job = TimerTask(callable_, *args, **kwargs)
54
+ self._jobs.append((job, time.time() + timeout))
55
+ self._jobs.sort(key=lambda j: j[1]) # sort on time
56
+ self.lock.notify()
57
+
58
+ return job
59
+ finally:
60
+ self.lock.release()
61
+
62
+ def cancel(self, timer_task):
63
+ self.lock.acquire()
64
+ try:
65
+ self._jobs = list(filter(lambda job: job[0] is not timer_task,
66
+ self._jobs))
67
+ self.lock.notify()
68
+ finally:
69
+ self.lock.release()
70
+
71
+ def shutdown(self, cancel_jobs=False):
72
+ self.lock.acquire()
73
+ try:
74
+ self.die = True
75
+ if cancel_jobs:
76
+ self._jobs = []
77
+ self.lock.notify()
78
+ finally:
79
+ self.lock.release()
80
+
81
+ def _get_sleep_time(self):
82
+ if not self._jobs:
83
+ return 0
84
+ else:
85
+ job, scheduled_at = self._jobs[0]
86
+ return scheduled_at - time.time()
87
+
88
+ def run(self):
89
+ while True:
90
+ self.lock.acquire()
91
+ job = None
92
+ try:
93
+ if not self._jobs:
94
+ if self.die:
95
+ break
96
+ else:
97
+ self.lock.wait()
98
+ elif self._get_sleep_time() > 0:
99
+ self.lock.wait(self._get_sleep_time())
100
+ else:
101
+ job, timeout = self._jobs.pop(0)
102
+ finally:
103
+ self.lock.release()
104
+
105
+ if job:
106
+ # invoke the task without holding the lock
107
+ job.run()
backlash/utils.py ADDED
@@ -0,0 +1,42 @@
1
+ from backlash._compat import text_type, binary_type
2
+ from random import SystemRandom
3
+
4
+ SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
5
+
6
+ _sys_rng = SystemRandom()
7
+
8
+ def escape(s, quote=False):
9
+ """Replace special characters "&", "<" and ">" to HTML-safe sequences. If
10
+ the optional flag `quote` is `True`, the quotation mark character is
11
+ also translated.
12
+
13
+ There is a special handling for `None` which escapes to an empty string.
14
+
15
+ :param s: the string to escape.
16
+ :param quote: set to true to also escape double quotes.
17
+ """
18
+ if s is None:
19
+ return ''
20
+ if hasattr(s, '__html__'):
21
+ return s.__html__()
22
+ if not isinstance(s, (text_type, binary_type)):
23
+ s = text_type(s)
24
+ if isinstance(s, binary_type):
25
+ try:
26
+ s.decode('ascii')
27
+ except:
28
+ s = s.decode('utf-8', 'replace')
29
+ s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
30
+ if quote:
31
+ s = s.replace('"', "&quot;")
32
+ return s
33
+
34
+ def gen_salt(length):
35
+ """Generate a random string of SALT_CHARS with specified ``length``."""
36
+ if length <= 0:
37
+ raise ValueError('requested salt of length <= 0')
38
+ return ''.join(_sys_rng.choice(SALT_CHARS) for _ in range(length))
39
+
40
+ class RequestContext(dict):
41
+ def __getattr__(self, attr):
42
+ return self[attr]
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: backlash
3
+ Version: 0.4.0
4
+ Summary: Standalone WebOb port of the Werkzeug Debugger with Python3 support meant to replace WebError in TurboGears2
5
+ Author-email: Alessandro Molina <alessandro@molina.fyi>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/TurboGears/backlash
8
+ Keywords: wsgi,debugger,webob,python3,web
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
13
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
14
+ Description-Content-Type: text/x-rst
15
+ License-File: LICENSE
16
+ License-File: AUTHORS
17
+ Requires-Dist: WebOb
18
+ Dynamic: license-file
19
+
20
+ About backlash
21
+ -------------------------
22
+
23
+ backlash is a swiss army knife for web applications debugging, which provides:
24
+
25
+ - An Interactive In Browser Debugger based on a Werkzeug Debugger fork ported to WebOb
26
+ - Crash reporting by email and on Sentry
27
+ - Slow requests reporting by email and on Sentry.
28
+
29
+ Backlash was born as a replacement for WebError in TurboGears2.3 versions.
30
+
31
+ Installing
32
+ -------------------------------
33
+
34
+ backlash can be installed from pypi::
35
+
36
+ pip install backlash
37
+
38
+ should just work for most of the users
39
+
40
+ Debugging and Console
41
+ ----------------------------------
42
+
43
+ Backlash supports both debugging applications on crash and realtime console,
44
+ both are based on the Werkzeug Debugger and adapted to work with WebOb.
45
+
46
+ The debugging function is provided by the ``DebuggedApplication`` middleware,
47
+ wrapping your application with this middleware will intercept any exception
48
+ and display the traceback and an interactive console in your browser.
49
+
50
+ An interactive console will also be always available at ``/__console__`` path.
51
+
52
+ Context Injectors
53
+ +++++++++++++++++++++++++++++
54
+
55
+ The ``DebuggedApplication`` middleware also makes possible to provide one or more
56
+ ``context injectors``, those are simple python functions that will be called when
57
+ an exception is raised to retrieve the context to store and make back available during
58
+ debugging.
59
+
60
+ Context injectors have to return a dictionary which will be merged into the current
61
+ request context, the request context itself will be made available inside the debugger
62
+ as the ``ctx`` object.
63
+
64
+ This feature is used for example by TurboGears to provide back some of the objects
65
+ which were available during execution like the current request.
66
+
67
+ Example
68
+ +++++++++++++++++++++++++++++++
69
+
70
+ The DebuggedApplication middleware is used by TurboGears in the following way::
71
+
72
+ def _turbogears_backlash_context(environ):
73
+ tgl = environ.get('tg.locals')
74
+ return {'request':getattr(tgl, 'request', None)}
75
+
76
+ app = backlash.DebuggedApplication(app, context_injectors=[_turbogears_backlash_context])
77
+
78
+
79
+ Exception Tracing
80
+ ---------------------------------------
81
+
82
+ The ``TraceErrorsMiddleware`` provides a WSGI middleware that intercepts any exception
83
+ raised during execution, retrieves a traceback object and provides it to one or more
84
+ ``reporters`` to log the error.
85
+
86
+ By default the ``EmailReporter`` and ``SentryReporter`` are provided to send error
87
+ reports by email and on Sentry.
88
+
89
+ The ``EmailReporter`` supports most of the options WebError ErrorMiddleware to provide some
90
+ kind of backward compatibility and make possible a quick transition.
91
+
92
+ While this function is easily replicable using the python logging SMTPHandler, the
93
+ TraceErrorsMiddleware is explicitly meant for web applications crash reporting
94
+ which has the benefit of being able to provide more complete information and keep a clear
95
+ and separate process in managing errors.
96
+
97
+ Example
98
+ ++++++++++++++++++++++++++++++++
99
+
100
+ The TraceErrorsMiddleware is used by TurboGears in the following way::
101
+
102
+ from backlash.trace_errors import EmailReporter
103
+
104
+ def _turbogears_backlash_context(environ):
105
+ tgl = environ.get('tg.locals')
106
+ return {'request':getattr(tgl, 'request', None)}
107
+
108
+ app = backlash.TraceErrorsMiddleware(app, [EmailReporter(**errorware)],
109
+ context_injectors=[_turbogears_backlash_context])
110
+
111
+ Slow Requests Tracing
112
+ ---------------------------------------
113
+
114
+ The ``TraceSlowRequestsMiddleware`` provides a WSGI middleware that tracks requests
115
+ execution time and reports requests that took more than a specified interval to complete
116
+ (by default 25 seconds).
117
+
118
+ It is also possible to exclude a list of paths that start with a specified string
119
+ to avoid reporting long polling connections or other kind of requests that are
120
+ expected to have a long life spawn.
121
+
122
+ Example
123
+ ++++++++++++++++++++++++++++++++
124
+
125
+ The TraceSlowRequestsMiddleware is used by TurboGears in the following way::
126
+
127
+ from backlash.trace_errors import EmailReporter
128
+
129
+ def _turbogears_backlash_context(environ):
130
+ tgl = environ.get('tg.locals')
131
+ return {'request':getattr(tgl, 'request', None)}
132
+
133
+ app = backlash.TraceSlowRequestsMiddleware(app, [EmailReporter(**errorware)],
134
+ interval=25, exclude_paths=None,
135
+ context_injectors=[_turbogears_backlash_context])