[FLASK] added request and response hook (#416)

This commit is contained in:
Nick
2021-06-01 14:44:24 -07:00
committed by GitHub
parent 5d1f3201af
commit fbd39ccaac
3 changed files with 83 additions and 30 deletions

View File

@ -73,6 +73,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation - `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299)) ([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
- `opentelemetry-instrumentation-flask` Added `request_hook` and `response_hook` callbacks.
([#416](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/416))
- `opentelemetry-instrumenation-django` now supports request and response hooks. - `opentelemetry-instrumenation-django` now supports request and response hooks.
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407)) ([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
- `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks. - `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks.

View File

@ -85,7 +85,7 @@ def get_default_span_name():
return span_name return span_name
def _rewrapped_app(wsgi_app): def _rewrapped_app(wsgi_app, response_hook=None):
def _wrapped_app(wrapped_app_environ, start_response): def _wrapped_app(wrapped_app_environ, start_response):
# We want to measure the time for route matching, etc. # We want to measure the time for route matching, etc.
# In theory, we could start the span here and use # In theory, we could start the span here and use
@ -114,7 +114,8 @@ def _rewrapped_app(wsgi_app):
"missing at _start_response(%s)", "missing at _start_response(%s)",
status, status,
) )
if response_hook is not None:
response_hook(span, status, response_headers)
return start_response(status, response_headers, *args, **kwargs) return start_response(status, response_headers, *args, **kwargs)
return wsgi_app(wrapped_app_environ, _start_response) return wsgi_app(wrapped_app_environ, _start_response)
@ -122,13 +123,12 @@ def _rewrapped_app(wsgi_app):
return _wrapped_app return _wrapped_app
def _wrapped_before_request(name_callback, tracer): def _wrapped_before_request(request_hook=None, tracer=None):
def _before_request(): def _before_request():
if _excluded_urls.url_disabled(flask.request.url): if _excluded_urls.url_disabled(flask.request.url):
return return
flask_request_environ = flask.request.environ flask_request_environ = flask.request.environ
span_name = name_callback() span_name = get_default_span_name()
token = context.attach( token = context.attach(
extract(flask_request_environ, getter=otel_wsgi.wsgi_getter) extract(flask_request_environ, getter=otel_wsgi.wsgi_getter)
) )
@ -138,6 +138,9 @@ def _wrapped_before_request(name_callback, tracer):
kind=trace.SpanKind.SERVER, kind=trace.SpanKind.SERVER,
start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY), start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
) )
if request_hook:
request_hook(span, flask_request_environ)
if span.is_recording(): if span.is_recording():
attributes = otel_wsgi.collect_request_attributes( attributes = otel_wsgi.collect_request_attributes(
flask_request_environ flask_request_environ
@ -183,21 +186,25 @@ def _teardown_request(exc):
class _InstrumentedFlask(flask.Flask): class _InstrumentedFlask(flask.Flask):
name_callback = get_default_span_name
_tracer_provider = None _tracer_provider = None
_request_hook = None
_response_hook = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._original_wsgi_ = self.wsgi_app self._original_wsgi_ = self.wsgi_app
self.wsgi_app = _rewrapped_app(self.wsgi_app)
self.wsgi_app = _rewrapped_app(
self.wsgi_app, _InstrumentedFlask._response_hook
)
tracer = trace.get_tracer( tracer = trace.get_tracer(
__name__, __version__, _InstrumentedFlask._tracer_provider __name__, __version__, _InstrumentedFlask._tracer_provider
) )
_before_request = _wrapped_before_request( _before_request = _wrapped_before_request(
_InstrumentedFlask.name_callback, tracer, _InstrumentedFlask._request_hook, tracer,
) )
self._before_request = _before_request self._before_request = _before_request
self.before_request(_before_request) self.before_request(_before_request)
@ -216,26 +223,30 @@ class FlaskInstrumentor(BaseInstrumentor):
def _instrument(self, **kwargs): def _instrument(self, **kwargs):
self._original_flask = flask.Flask self._original_flask = flask.Flask
name_callback = kwargs.get("name_callback") request_hook = kwargs.get("request_hook")
response_hook = kwargs.get("response_hook")
if callable(request_hook):
_InstrumentedFlask._request_hook = request_hook
if callable(response_hook):
_InstrumentedFlask._response_hook = response_hook
flask.Flask = _InstrumentedFlask
tracer_provider = kwargs.get("tracer_provider") tracer_provider = kwargs.get("tracer_provider")
if callable(name_callback):
_InstrumentedFlask.name_callback = name_callback
_InstrumentedFlask._tracer_provider = tracer_provider _InstrumentedFlask._tracer_provider = tracer_provider
flask.Flask = _InstrumentedFlask flask.Flask = _InstrumentedFlask
def instrument_app( def instrument_app(
self, app, name_callback=get_default_span_name, tracer_provider=None self, app, request_hook=None, response_hook=None, tracer_provider=None
): # pylint: disable=no-self-use ): # pylint: disable=no-self-use
if not hasattr(app, "_is_instrumented"): if not hasattr(app, "_is_instrumented"):
app._is_instrumented = False app._is_instrumented = False
if not app._is_instrumented: if not app._is_instrumented:
app._original_wsgi_app = app.wsgi_app app._original_wsgi_app = app.wsgi_app
app.wsgi_app = _rewrapped_app(app.wsgi_app) app.wsgi_app = _rewrapped_app(app.wsgi_app, response_hook)
tracer = trace.get_tracer(__name__, __version__, tracer_provider) tracer = trace.get_tracer(__name__, __version__, tracer_provider)
_before_request = _wrapped_before_request(name_callback, tracer) _before_request = _wrapped_before_request(request_hook, tracer)
app._before_request = _before_request app._before_request = _before_request
app.before_request(_before_request) app.before_request(_before_request)
app.teardown_request(_teardown_request) app.teardown_request(_teardown_request)

View File

@ -220,19 +220,28 @@ class TestProgrammatic(InstrumentationTest, TestBase, WsgiTestBase):
self.assertEqual(len(span_list), 1) self.assertEqual(len(span_list), 1)
class TestProgrammaticCustomSpanName( class TestProgrammaticHooks(InstrumentationTest, TestBase, WsgiTestBase):
InstrumentationTest, TestBase, WsgiTestBase
):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
def custom_span_name(): hook_headers = (
return "flask-custom-span-name" "hook_attr",
"hello otel",
)
def request_hook_test(span, environ):
span.update_name("name from hook")
def response_hook_test(span, environ, response_headers):
span.set_attribute("hook_attr", "hello world")
response_headers.append(hook_headers)
self.app = Flask(__name__) self.app = Flask(__name__)
FlaskInstrumentor().instrument_app( FlaskInstrumentor().instrument_app(
self.app, name_callback=custom_span_name self.app,
request_hook=request_hook_test,
response_hook=response_hook_test,
) )
self._common_initialization() self._common_initialization()
@ -242,24 +251,44 @@ class TestProgrammaticCustomSpanName(
with self.disable_logging(): with self.disable_logging():
FlaskInstrumentor().uninstrument_app(self.app) FlaskInstrumentor().uninstrument_app(self.app)
def test_custom_span_name(self): def test_hooks(self):
self.client.get("/hello/123") expected_attrs = expected_attributes(
{
"http.target": "/hello/123",
"http.route": "/hello/<int:helloid>",
"hook_attr": "hello world",
}
)
resp = self.client.get("/hello/123")
span_list = self.memory_exporter.get_finished_spans() span_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(span_list), 1) self.assertEqual(len(span_list), 1)
self.assertEqual(span_list[0].name, "flask-custom-span-name") self.assertEqual(span_list[0].name, "name from hook")
self.assertEqual(span_list[0].attributes, expected_attrs)
self.assertEqual(resp.headers["hook_attr"], "hello otel")
class TestProgrammaticCustomSpanNameCallbackWithoutApp( class TestProgrammaticHooksWithoutApp(
InstrumentationTest, TestBase, WsgiTestBase InstrumentationTest, TestBase, WsgiTestBase
): ):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
def custom_span_name(): hook_headers = (
return "instrument-without-app" "hook_attr",
"hello otel without app",
)
FlaskInstrumentor().instrument(name_callback=custom_span_name) def request_hook_test(span, environ):
span.update_name("without app")
def response_hook_test(span, environ, response_headers):
span.set_attribute("hook_attr", "hello world without app")
response_headers.append(hook_headers)
FlaskInstrumentor().instrument(
request_hook=request_hook_test, response_hook=response_hook_test
)
# pylint: disable=import-outside-toplevel,reimported,redefined-outer-name # pylint: disable=import-outside-toplevel,reimported,redefined-outer-name
from flask import Flask from flask import Flask
@ -272,12 +301,21 @@ class TestProgrammaticCustomSpanNameCallbackWithoutApp(
with self.disable_logging(): with self.disable_logging():
FlaskInstrumentor().uninstrument() FlaskInstrumentor().uninstrument()
def test_custom_span_name(self): def test_no_app_hooks(self):
self.client.get("/hello/123") expected_attrs = expected_attributes(
{
"http.target": "/hello/123",
"http.route": "/hello/<int:helloid>",
"hook_attr": "hello world without app",
}
)
resp = self.client.get("/hello/123")
span_list = self.memory_exporter.get_finished_spans() span_list = self.memory_exporter.get_finished_spans()
self.assertEqual(len(span_list), 1) self.assertEqual(len(span_list), 1)
self.assertEqual(span_list[0].name, "instrument-without-app") self.assertEqual(span_list[0].name, "without app")
self.assertEqual(span_list[0].attributes, expected_attrs)
self.assertEqual(resp.headers["hook_attr"], "hello otel without app")
class TestProgrammaticCustomTracerProvider( class TestProgrammaticCustomTracerProvider(