Skip to content

Commit fbd39cc

Browse files
authored
[FLASK] added request and response hook (#416)
1 parent 5d1f320 commit fbd39cc

File tree

3 files changed

+83
-30
lines changed

3 files changed

+83
-30
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7373
### Added
7474
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
7575
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
76+
77+
- `opentelemetry-instrumentation-flask` Added `request_hook` and `response_hook` callbacks.
78+
([#416](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/416))
79+
7680
- `opentelemetry-instrumenation-django` now supports request and response hooks.
7781
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
7882
- `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks.

instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py

+25-14
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def get_default_span_name():
8585
return span_name
8686

8787

88-
def _rewrapped_app(wsgi_app):
88+
def _rewrapped_app(wsgi_app, response_hook=None):
8989
def _wrapped_app(wrapped_app_environ, start_response):
9090
# We want to measure the time for route matching, etc.
9191
# In theory, we could start the span here and use
@@ -114,21 +114,21 @@ def _start_response(status, response_headers, *args, **kwargs):
114114
"missing at _start_response(%s)",
115115
status,
116116
)
117-
117+
if response_hook is not None:
118+
response_hook(span, status, response_headers)
118119
return start_response(status, response_headers, *args, **kwargs)
119120

120121
return wsgi_app(wrapped_app_environ, _start_response)
121122

122123
return _wrapped_app
123124

124125

125-
def _wrapped_before_request(name_callback, tracer):
126+
def _wrapped_before_request(request_hook=None, tracer=None):
126127
def _before_request():
127128
if _excluded_urls.url_disabled(flask.request.url):
128129
return
129-
130130
flask_request_environ = flask.request.environ
131-
span_name = name_callback()
131+
span_name = get_default_span_name()
132132
token = context.attach(
133133
extract(flask_request_environ, getter=otel_wsgi.wsgi_getter)
134134
)
@@ -138,6 +138,9 @@ def _before_request():
138138
kind=trace.SpanKind.SERVER,
139139
start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
140140
)
141+
if request_hook:
142+
request_hook(span, flask_request_environ)
143+
141144
if span.is_recording():
142145
attributes = otel_wsgi.collect_request_attributes(
143146
flask_request_environ
@@ -183,21 +186,25 @@ def _teardown_request(exc):
183186

184187
class _InstrumentedFlask(flask.Flask):
185188

186-
name_callback = get_default_span_name
187189
_tracer_provider = None
190+
_request_hook = None
191+
_response_hook = None
188192

189193
def __init__(self, *args, **kwargs):
190194
super().__init__(*args, **kwargs)
191195

192196
self._original_wsgi_ = self.wsgi_app
193-
self.wsgi_app = _rewrapped_app(self.wsgi_app)
197+
198+
self.wsgi_app = _rewrapped_app(
199+
self.wsgi_app, _InstrumentedFlask._response_hook
200+
)
194201

195202
tracer = trace.get_tracer(
196203
__name__, __version__, _InstrumentedFlask._tracer_provider
197204
)
198205

199206
_before_request = _wrapped_before_request(
200-
_InstrumentedFlask.name_callback, tracer,
207+
_InstrumentedFlask._request_hook, tracer,
201208
)
202209
self._before_request = _before_request
203210
self.before_request(_before_request)
@@ -216,26 +223,30 @@ def instrumentation_dependencies(self) -> Collection[str]:
216223

217224
def _instrument(self, **kwargs):
218225
self._original_flask = flask.Flask
219-
name_callback = kwargs.get("name_callback")
226+
request_hook = kwargs.get("request_hook")
227+
response_hook = kwargs.get("response_hook")
228+
if callable(request_hook):
229+
_InstrumentedFlask._request_hook = request_hook
230+
if callable(response_hook):
231+
_InstrumentedFlask._response_hook = response_hook
232+
flask.Flask = _InstrumentedFlask
220233
tracer_provider = kwargs.get("tracer_provider")
221-
if callable(name_callback):
222-
_InstrumentedFlask.name_callback = name_callback
223234
_InstrumentedFlask._tracer_provider = tracer_provider
224235
flask.Flask = _InstrumentedFlask
225236

226237
def instrument_app(
227-
self, app, name_callback=get_default_span_name, tracer_provider=None
238+
self, app, request_hook=None, response_hook=None, tracer_provider=None
228239
): # pylint: disable=no-self-use
229240
if not hasattr(app, "_is_instrumented"):
230241
app._is_instrumented = False
231242

232243
if not app._is_instrumented:
233244
app._original_wsgi_app = app.wsgi_app
234-
app.wsgi_app = _rewrapped_app(app.wsgi_app)
245+
app.wsgi_app = _rewrapped_app(app.wsgi_app, response_hook)
235246

236247
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
237248

238-
_before_request = _wrapped_before_request(name_callback, tracer)
249+
_before_request = _wrapped_before_request(request_hook, tracer)
239250
app._before_request = _before_request
240251
app.before_request(_before_request)
241252
app.teardown_request(_teardown_request)

instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py

+54-16
Original file line numberDiff line numberDiff line change
@@ -220,19 +220,28 @@ def test_exclude_lists(self):
220220
self.assertEqual(len(span_list), 1)
221221

222222

223-
class TestProgrammaticCustomSpanName(
224-
InstrumentationTest, TestBase, WsgiTestBase
225-
):
223+
class TestProgrammaticHooks(InstrumentationTest, TestBase, WsgiTestBase):
226224
def setUp(self):
227225
super().setUp()
228226

229-
def custom_span_name():
230-
return "flask-custom-span-name"
227+
hook_headers = (
228+
"hook_attr",
229+
"hello otel",
230+
)
231+
232+
def request_hook_test(span, environ):
233+
span.update_name("name from hook")
234+
235+
def response_hook_test(span, environ, response_headers):
236+
span.set_attribute("hook_attr", "hello world")
237+
response_headers.append(hook_headers)
231238

232239
self.app = Flask(__name__)
233240

234241
FlaskInstrumentor().instrument_app(
235-
self.app, name_callback=custom_span_name
242+
self.app,
243+
request_hook=request_hook_test,
244+
response_hook=response_hook_test,
236245
)
237246

238247
self._common_initialization()
@@ -242,24 +251,44 @@ def tearDown(self):
242251
with self.disable_logging():
243252
FlaskInstrumentor().uninstrument_app(self.app)
244253

245-
def test_custom_span_name(self):
246-
self.client.get("/hello/123")
254+
def test_hooks(self):
255+
expected_attrs = expected_attributes(
256+
{
257+
"http.target": "/hello/123",
258+
"http.route": "/hello/<int:helloid>",
259+
"hook_attr": "hello world",
260+
}
261+
)
247262

263+
resp = self.client.get("/hello/123")
248264
span_list = self.memory_exporter.get_finished_spans()
249265
self.assertEqual(len(span_list), 1)
250-
self.assertEqual(span_list[0].name, "flask-custom-span-name")
266+
self.assertEqual(span_list[0].name, "name from hook")
267+
self.assertEqual(span_list[0].attributes, expected_attrs)
268+
self.assertEqual(resp.headers["hook_attr"], "hello otel")
251269

252270

253-
class TestProgrammaticCustomSpanNameCallbackWithoutApp(
271+
class TestProgrammaticHooksWithoutApp(
254272
InstrumentationTest, TestBase, WsgiTestBase
255273
):
256274
def setUp(self):
257275
super().setUp()
258276

259-
def custom_span_name():
260-
return "instrument-without-app"
277+
hook_headers = (
278+
"hook_attr",
279+
"hello otel without app",
280+
)
281+
282+
def request_hook_test(span, environ):
283+
span.update_name("without app")
284+
285+
def response_hook_test(span, environ, response_headers):
286+
span.set_attribute("hook_attr", "hello world without app")
287+
response_headers.append(hook_headers)
261288

262-
FlaskInstrumentor().instrument(name_callback=custom_span_name)
289+
FlaskInstrumentor().instrument(
290+
request_hook=request_hook_test, response_hook=response_hook_test
291+
)
263292
# pylint: disable=import-outside-toplevel,reimported,redefined-outer-name
264293
from flask import Flask
265294

@@ -272,12 +301,21 @@ def tearDown(self):
272301
with self.disable_logging():
273302
FlaskInstrumentor().uninstrument()
274303

275-
def test_custom_span_name(self):
276-
self.client.get("/hello/123")
304+
def test_no_app_hooks(self):
305+
expected_attrs = expected_attributes(
306+
{
307+
"http.target": "/hello/123",
308+
"http.route": "/hello/<int:helloid>",
309+
"hook_attr": "hello world without app",
310+
}
311+
)
312+
resp = self.client.get("/hello/123")
277313

278314
span_list = self.memory_exporter.get_finished_spans()
279315
self.assertEqual(len(span_list), 1)
280-
self.assertEqual(span_list[0].name, "instrument-without-app")
316+
self.assertEqual(span_list[0].name, "without app")
317+
self.assertEqual(span_list[0].attributes, expected_attrs)
318+
self.assertEqual(resp.headers["hook_attr"], "hello otel without app")
281319

282320

283321
class TestProgrammaticCustomTracerProvider(

0 commit comments

Comments
 (0)