Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit ab3f3d5

Browse files
wkiserliyanhui1228
authored andcommitted
Attach stack traces and exceptions to Flask traces (#151)
1 parent 1007659 commit ab3f3d5

2 files changed

Lines changed: 77 additions & 5 deletions

File tree

opencensus/trace/ext/flask/flask_middleware.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,21 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import flask
1615
import logging
16+
import sys
17+
18+
import flask
19+
from google.rpc import code_pb2
1720

1821
from opencensus.trace import attributes_helper
1922
from opencensus.trace import execution_context
20-
from opencensus.trace.propagation import google_cloud_format
23+
from opencensus.trace import stack_trace
24+
from opencensus.trace import status
25+
from opencensus.trace import tracer as tracer_module
2126
from opencensus.trace.exporters import print_exporter
2227
from opencensus.trace.ext import utils
28+
from opencensus.trace.propagation import google_cloud_format
2329
from opencensus.trace.samplers import always_on
24-
from opencensus.trace import tracer as tracer_module
2530

2631
_FLASK_TRACE_HEADER = 'X_CLOUD_TRACE_CONTEXT'
2732

@@ -81,6 +86,7 @@ def __init__(self, app, blacklist_paths=None, sampler=None, exporter=None,
8186
def setup_trace(self):
8287
self.app.before_request(self._before_request)
8388
self.app.after_request(self._after_request)
89+
self.app.teardown_request(self._teardown_request)
8490

8591
def _before_request(self):
8692
"""A function to be run before each request.
@@ -127,13 +133,38 @@ def _after_request(self, response):
127133
tracer.add_attribute_to_current_span(
128134
HTTP_STATUS_CODE,
129135
str(response.status_code))
136+
except Exception: # pragma: NO COVER
137+
log.error('Failed to trace request', exc_info=True)
138+
finally:
139+
return response
140+
141+
def _teardown_request(self, exception):
142+
# Do not trace if the url is blacklisted
143+
if utils.disable_tracing_url(flask.request.url, self.blacklist_paths):
144+
return
145+
146+
try:
147+
tracer = execution_context.get_opencensus_tracer()
148+
149+
if exception is not None:
150+
span = execution_context.get_current_span()
151+
span.status = status.Status(
152+
code=code_pb2.UNKNOWN,
153+
message=str(exception)
154+
)
155+
# try attaching the stack trace to the span, only populated if
156+
# the app has 'PROPAGATE_EXCEPTIONS', 'DEBUG', or 'TESTING'
157+
# enabled
158+
exc_type, _, exc_traceback = sys.exc_info()
159+
if exc_traceback is not None:
160+
span.stack_trace = stack_trace.StackTrace.from_traceback(
161+
exc_traceback
162+
)
130163

131164
tracer.end_span()
132165
tracer.finish()
133166
except Exception: # pragma: NO COVER
134167
log.error('Failed to trace request', exc_info=True)
135-
finally:
136-
return response
137168

138169

139170
def get_flask_header():

tests/unit/trace/ext/flask/test_flask_middleware.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919

2020
import flask
2121
import mock
22+
from google.rpc import code_pb2
2223

2324
from opencensus.trace import execution_context
25+
from opencensus.trace import span_data
26+
from opencensus.trace import stack_trace
27+
from opencensus.trace import status
2428
from opencensus.trace.exporters import print_exporter
2529
from opencensus.trace.ext.flask import flask_middleware
2630
from opencensus.trace.propagation import google_cloud_format
@@ -44,6 +48,10 @@ def index():
4448
def health_check():
4549
return 'test health check' # pragma: NO COVER
4650

51+
@app.route('/error')
52+
def error():
53+
raise Exception('error')
54+
4755
return app
4856

4957
def tearDown(self):
@@ -241,3 +249,36 @@ def test__after_request_blacklist(self):
241249

242250
self.assertEqual(response.status_code, 200)
243251
assert isinstance(tracer, noop_tracer.NoopTracer)
252+
253+
def test_teardown_include_exception(self):
254+
mock_exporter = mock.MagicMock()
255+
app = self.create_app()
256+
flask_middleware.FlaskMiddleware(app=app, exporter=mock_exporter)
257+
response = app.test_client().get('/error')
258+
259+
self.assertEqual(response.status_code, 500)
260+
261+
exported_spandata = mock_exporter.export.call_args[0][0][0]
262+
self.assertIsInstance(exported_spandata, span_data.SpanData)
263+
self.assertIsInstance(exported_spandata.status, status.Status)
264+
self.assertEqual(exported_spandata.status.code, code_pb2.UNKNOWN)
265+
self.assertEqual(exported_spandata.status.message, 'error')
266+
267+
def test_teardown_include_exception_and_traceback(self):
268+
mock_exporter = mock.MagicMock()
269+
app = self.create_app()
270+
app.config['TESTING'] = True
271+
flask_middleware.FlaskMiddleware(app=app, exporter=mock_exporter)
272+
with self.assertRaises(Exception):
273+
app.test_client().get('/error')
274+
275+
exported_spandata = mock_exporter.export.call_args[0][0][0]
276+
self.assertIsInstance(exported_spandata, span_data.SpanData)
277+
self.assertIsInstance(exported_spandata.status, status.Status)
278+
self.assertEqual(exported_spandata.status.code, code_pb2.UNKNOWN)
279+
self.assertEqual(exported_spandata.status.message, 'error')
280+
self.assertIsInstance(
281+
exported_spandata.stack_trace, stack_trace.StackTrace
282+
)
283+
self.assertIsNotNone(exported_spandata.stack_trace.stack_trace_hash_id)
284+
self.assertNotEqual(exported_spandata.stack_trace.stack_frames, [])

0 commit comments

Comments
 (0)