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.
- backlash/__init__.py +3 -0
- backlash/_compat.py +78 -0
- backlash/console.py +213 -0
- backlash/debug.py +193 -0
- backlash/frtools.py +27 -0
- backlash/repr.py +305 -0
- backlash/statics/FONT_LICENSE +202 -0
- backlash/statics/console.png +0 -0
- backlash/statics/debugger.js +202 -0
- backlash/statics/jquery.js +167 -0
- backlash/statics/less.png +0 -0
- backlash/statics/more.png +0 -0
- backlash/statics/opensans.ttf +0 -0
- backlash/statics/source.png +0 -0
- backlash/statics/style.css +113 -0
- backlash/tbtools.py +526 -0
- backlash/trace_errors/__init__.py +8 -0
- backlash/trace_errors/sentry.py +6 -0
- backlash/tracing/__init__.py +1 -0
- backlash/tracing/errors/__init__.py +1 -0
- backlash/tracing/errors/middleware.py +85 -0
- backlash/tracing/reporters/__init__.py +1 -0
- backlash/tracing/reporters/mail.py +126 -0
- backlash/tracing/reporters/sentry.py +40 -0
- backlash/tracing/slowrequests/__init__.py +1 -0
- backlash/tracing/slowrequests/middleware.py +113 -0
- backlash/tracing/slowrequests/timer.py +107 -0
- backlash/utils.py +42 -0
- backlash-0.4.0.dist-info/METADATA +135 -0
- backlash-0.4.0.dist-info/RECORD +34 -0
- backlash-0.4.0.dist-info/WHEEL +5 -0
- backlash-0.4.0.dist-info/licenses/AUTHORS +15 -0
- backlash-0.4.0.dist-info/licenses/LICENSE +7 -0
- backlash-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -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('&', '&').replace('<', '<').replace('>', '>')
|
|
30
|
+
if quote:
|
|
31
|
+
s = s.replace('"', """)
|
|
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])
|