Skip to content

Commit a853f1b

Browse files
author
Emanuele Palazzetti
authored
Merge pull request #409 from clutchski/flask-errs
flask: don't override code of already handled errors
2 parents a03b415 + 02ec844 commit a853f1b

File tree

2 files changed

+159
-94
lines changed

2 files changed

+159
-94
lines changed

ddtrace/contrib/flask/middleware.py

Lines changed: 101 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""
2-
Datadog trace code for flask.
2+
Datadog tracing code for flask.
33
4-
Requires a modern version of flask and the `blinker` library (which is a
5-
dependency of flask signals).
4+
Installing the blinker library will allow the tracing middleware to collect
5+
more exception info.
66
"""
77

88
# stdlib
@@ -21,54 +21,46 @@
2121
log = logging.getLogger(__name__)
2222

2323

24+
SPAN_NAME = 'flask.request'
25+
26+
2427
class TraceMiddleware(object):
2528

2629
def __init__(self, app, tracer, service="flask", use_signals=True, distributed_tracing=False):
2730
self.app = app
2831
self.app.logger.info("initializing trace middleware")
2932

30-
# save our traces.
3133
self._tracer = tracer
3234
self._service = service
3335
self._use_distributed_tracing = distributed_tracing
36+
self.use_signals = use_signals
3437

3538
self._tracer.set_service_info(
3639
service=service,
3740
app="flask",
3841
app_type=AppTypes.web,
3942
)
4043

41-
# warn the user if signals are unavailable (because blinker isn't
42-
# installed) if they are asking to use them.
44+
# Install hooks which time requests.
45+
self.app.before_request(self._before_request)
46+
self.app.after_request(self._after_request)
47+
self.app.teardown_request(self._teardown_request)
48+
49+
# Add exception handling signals. This will annotate exceptions that
50+
# are caught and handled in custom user code.
51+
# See https://github.com/DataDog/dd-trace-py/issues/390
4352
if use_signals and not signals.signals_available:
4453
self.app.logger.info(_blinker_not_installed_msg)
4554
self.use_signals = use_signals and signals.signals_available
46-
47-
# our signal receivers
48-
self._receivers = []
49-
50-
# instrument request timings
5155
timing_signals = {
52-
'request_started': self._request_started,
53-
'request_finished': self._request_finished,
5456
'got_request_exception': self._request_exception,
5557
}
58+
self._receivers = []
5659
if self.use_signals and _signals_exist(timing_signals):
5760
self._connect(timing_signals)
58-
else:
59-
# Fallback to request hooks. Won't catch exceptions.
60-
# handle exceptions.
61-
self.app.before_request(self._before_request)
62-
self.app.after_request(self._after_request)
6361

6462
_patch_render(tracer)
6563

66-
def _flask_signals_exist(self, names):
67-
""" Return true if the current version of flask has all of the given
68-
signals.
69-
"""
70-
return all(getattr(signals, n, None) for n in names)
71-
7264
def _connect(self, signal_to_handler):
7365
connected = True
7466
for name, handler in signal_to_handler.items():
@@ -85,7 +77,34 @@ def _connect(self, signal_to_handler):
8577
self._receivers.append(handler)
8678
return connected
8779

88-
# common methods
80+
def _before_request(self):
81+
""" Starts tracing the current request and stores it in the global
82+
request object.
83+
"""
84+
self._start_span()
85+
86+
def _after_request(self, response):
87+
""" Runs after the server can process a response. """
88+
try:
89+
self._process_response(response)
90+
except Exception:
91+
self.app.logger.exception("error tracing response")
92+
return response
93+
94+
def _teardown_request(self, exception):
95+
""" Runs at the end of a request. If there's an unhandled exception, it
96+
will be passed in.
97+
"""
98+
# when we teardown the span, ensure we have a clean slate.
99+
span = getattr(g, 'flask_datadog_span', None)
100+
setattr(g, 'flask_datadog_span', None)
101+
if not span:
102+
return
103+
104+
try:
105+
self._finish_span(span, exception=exception)
106+
except Exception:
107+
self.app.logger.exception("error finishing span")
89108

90109
def _start_span(self):
91110
if self._use_distributed_tracing:
@@ -96,85 +115,76 @@ def _start_span(self):
96115
self._tracer.context_provider.activate(context)
97116
try:
98117
g.flask_datadog_span = self._tracer.trace(
99-
"flask.request",
118+
SPAN_NAME,
100119
service=self._service,
101120
span_type=http.TYPE,
102121
)
103122
except Exception:
104123
self.app.logger.exception("error tracing request")
105124

106-
def _finish_span(self, response=None, exception=None):
107-
""" Close and finish the active span if it exists. """
125+
def _process_response(self, response):
108126
span = getattr(g, 'flask_datadog_span', None)
109-
if span:
110-
if span.sampled:
111-
error = 0
112-
code = response.status_code if response else None
113-
method = request.method if request else None
114-
115-
# if we didn't get a response, but we did get an exception, set
116-
# codes accordingly.
117-
if not response and exception:
118-
code = 500
119-
# The 3 next lines might not be strictly required, since `set_traceback`
120-
# also get the exception from the sys.exc_info (and fill the error meta).
121-
# Since we aren't sure it always work/for insuring no BC break, keep
122-
# these lines which get overridden anyway.
123-
error = 1
124-
span.set_tag(errors.ERROR_TYPE, type(exception))
125-
span.set_tag(errors.ERROR_MSG, exception)
126-
# The provided `exception` object doesn't have a stack trace attached,
127-
# so attach the stack trace with `set_traceback`.
128-
span.set_traceback()
129-
130-
# the endpoint that matched the request is None if an exception
131-
# happened so we fallback to a common resource
132-
resource = code if not request.endpoint else request.endpoint
133-
span.resource = compat.to_unicode(resource).lower()
134-
span.set_tag(http.URL, compat.to_unicode(request.base_url or ''))
135-
span.set_tag(http.STATUS_CODE, code)
136-
span.set_tag(http.METHOD, method)
137-
span.error = error
138-
span.finish()
139-
# Clear our span just in case.
140-
g.flask_datadog_span = None
141-
142-
# Request hook methods
127+
if not (span and span.sampled):
128+
return
143129

144-
def _before_request(self):
145-
""" Starts tracing the current request and stores it in the global
146-
request object.
147-
"""
148-
self._start_span()
130+
code = response.status_code if response else ''
131+
span.set_tag(http.STATUS_CODE, code)
149132

150-
def _after_request(self, response):
151-
""" handles a successful response. """
152-
try:
153-
self._finish_span(response=response)
154-
except Exception:
155-
self.app.logger.exception("error finishing trace")
156-
finally:
157-
return response
158-
159-
# signal handling methods
160-
161-
def _request_started(self, sender):
162-
self._start_span()
133+
def _request_exception(self, *args, **kwargs):
134+
exception = kwargs.get("exception", None)
135+
span = getattr(g, 'flask_datadog_span', None)
136+
if span and exception:
137+
_set_error_on_span(span, exception)
163138

164-
def _request_finished(self, sender, response, **kwargs):
165-
try:
166-
self._finish_span(response=response)
167-
except Exception:
168-
self.app.logger.exception("error finishing trace")
169-
return response
139+
def _finish_span(self, span, exception=None):
140+
if not span or not span.sampled:
141+
return
170142

171-
def _request_exception(self, *args, **kwargs):
172-
""" handles an error response. """
173-
exception = kwargs.pop("exception", None)
143+
code = span.get_tag(http.STATUS_CODE) or 0
174144
try:
175-
self._finish_span(exception=exception)
145+
code = int(code)
176146
except Exception:
177-
self.app.logger.exception("error tracing error")
147+
code = 0
148+
149+
if exception:
150+
# if the request has already had a code set, don't override it.
151+
code = code or 500
152+
_set_error_on_span(span, exception)
153+
154+
# the endpoint that matched the request is None if an exception
155+
# happened so we fallback to a common resource
156+
span.error = 0 if code < 500 else 1
157+
158+
# the request isn't guaranteed to exist here, so only use it carefully.
159+
method = ''
160+
endpoint = ''
161+
url = ''
162+
if request:
163+
method = request.method
164+
endpoint = request.endpoint or code
165+
url = request.base_url or ''
166+
167+
# Let users specify their own resource in middleware if they so desire.
168+
# See case https://github.com/DataDog/dd-trace-py/issues/353
169+
if span.resource == SPAN_NAME:
170+
resource = endpoint or code
171+
span.resource = compat.to_unicode(resource).lower()
172+
173+
span.set_tag(http.URL, compat.to_unicode(url))
174+
span.set_tag(http.STATUS_CODE, code)
175+
span.set_tag(http.METHOD, method)
176+
span.finish()
177+
178+
def _set_error_on_span(span, exception):
179+
# The 3 next lines might not be strictly required, since `set_traceback`
180+
# also get the exception from the sys.exc_info (and fill the error meta).
181+
# Since we aren't sure it always work/for insuring no BC break, keep
182+
# these lines which get overridden anyway.
183+
span.set_tag(errors.ERROR_TYPE, type(exception))
184+
span.set_tag(errors.ERROR_MSG, exception)
185+
# The provided `exception` object doesn't have a stack trace attached,
186+
# so attach the stack trace with `set_traceback`.
187+
span.set_traceback()
178188

179189
def _patch_render(tracer):
180190
""" patch flask's render template methods with the given tracer. """

tests/contrib/flask/test_flask.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from flask import Flask, render_template
1010
from nose.tools import eq_
1111

12+
1213
# project
1314
from ddtrace import Tracer
1415
from ddtrace.constants import SAMPLING_PRIORITY_KEY
@@ -25,8 +26,8 @@
2526
tracer.writer = writer
2627

2728

28-
class TestError(Exception):
29-
pass
29+
class TestError(Exception): pass
30+
class HandleMe(Exception): pass
3031

3132

3233
# define a toy flask app.
@@ -45,6 +46,9 @@ def index():
4546
def error():
4647
raise TestError()
4748

49+
@app.route('/handleme')
50+
def handle_me():
51+
raise HandleMe()
4852

4953
@app.route('/fatal')
5054
def fatal():
@@ -70,6 +74,13 @@ def child():
7074
span.set_tag('a', 'b')
7175
return 'child'
7276

77+
@app.route("/custom_span")
78+
def custom_span():
79+
span = tracer.current_span()
80+
assert span
81+
span.resource = "overridden"
82+
return 'hiya'
83+
7384

7485
def unicode_view():
7586
return u'üŋïĉóđē'
@@ -87,6 +98,11 @@ def handle_my_exception(e):
8798
assert isinstance(e, TestError)
8899
return 'error', 500
89100

101+
@app.errorhandler(HandleMe)
102+
def err_to_202(e):
103+
assert isinstance(e, HandleMe)
104+
return 'handled', 202
105+
90106

91107
# add tracing to the app (we use a global app to help ensure multiple requests
92108
# work)
@@ -195,6 +211,28 @@ def test_template(self):
195211
eq_(t.trace_id, s.trace_id)
196212
assert s.start < t.start < t.start + t.duration < end
197213

214+
def test_handleme(self):
215+
start = time.time()
216+
rv = app.get('/handleme')
217+
end = time.time()
218+
219+
# ensure request worked
220+
eq_(rv.status_code, 202)
221+
eq_(rv.data, b'handled')
222+
223+
# ensure trace worked
224+
assert not tracer.current_span(), tracer.current_span().pprint()
225+
spans = writer.pop()
226+
eq_(len(spans), 1)
227+
s = spans[0]
228+
eq_(s.service, service)
229+
eq_(s.resource, "handle_me")
230+
assert s.start >= start
231+
assert s.duration <= end - start
232+
eq_(s.error, 0)
233+
eq_(s.meta.get(http.STATUS_CODE), '202')
234+
eq_(s.meta.get(http.METHOD), 'GET')
235+
198236
def test_template_err(self):
199237
start = time.time()
200238
try:
@@ -294,7 +332,7 @@ def test_fatal(self):
294332
assert s.duration <= end - start
295333
eq_(s.meta.get(http.STATUS_CODE), '500')
296334
eq_(s.meta.get(http.METHOD), 'GET')
297-
assert "ZeroDivisionError" in s.meta.get(errors.ERROR_TYPE)
335+
assert "ZeroDivisionError" in s.meta.get(errors.ERROR_TYPE), s.meta
298336
assert "by zero" in s.meta.get(errors.ERROR_MSG)
299337
assert re.search('File ".*/contrib/flask/test_flask.py", line [0-9]+, in fatal', s.meta.get(errors.ERROR_STACK))
300338

@@ -364,3 +402,20 @@ def test_propagation(self):
364402
eq_(s.trace_id, 1234)
365403
eq_(s.parent_id, 4567)
366404
eq_(s.get_metric(SAMPLING_PRIORITY_KEY), 2)
405+
406+
def test_custom_span(self):
407+
rv = app.get('/custom_span')
408+
eq_(rv.status_code, 200)
409+
# ensure trace worked
410+
assert not tracer.current_span(), tracer.current_span().pprint()
411+
spans = writer.pop()
412+
eq_(len(spans), 1)
413+
s = spans[0]
414+
eq_(s.service, service)
415+
eq_(s.resource, "overridden")
416+
eq_(s.error, 0)
417+
eq_(s.meta.get(http.STATUS_CODE), '200')
418+
eq_(s.meta.get(http.METHOD), 'GET')
419+
420+
421+

0 commit comments

Comments
 (0)