Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FLASK] added request and response hook #416

Merged
merged 42 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a0cdcc2
added request and response hook - may be very buggy
NickSulistio Apr 6, 2021
53ddfcd
Merge branch 'main' into flask_hooks
NickSulistio Apr 8, 2021
c0ab6a5
added req/resp hooks and tests
NickSulistio Apr 8, 2021
ddb3077
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
NickSulistio Apr 8, 2021
ea965d1
Merge branch 'flask_hooks' of https://github.com/nicksulistio/opentel…
NickSulistio Apr 8, 2021
74bc40c
Merge branch 'main' into flask_hooks
NickSulistio Apr 9, 2021
8a4f2a3
Update CHANGELOG.md
NickSulistio Apr 12, 2021
4721cdf
removed name callback
NickSulistio Apr 13, 2021
79d04fe
Merge branch 'main' into flask_hooks
NickSulistio Apr 13, 2021
f975067
Merge branch 'main' into flask_hooks
lzchen Apr 15, 2021
1605884
Merge branch 'main' into flask_hooks
NickSulistio Apr 16, 2021
7bffdba
added response header tests
NickSulistio May 24, 2021
84bbec1
Merge branch 'main' into flask_hooks
NickSulistio May 25, 2021
5eaeaf2
solved merge conflicts
NickSulistio May 25, 2021
42e9372
fixed more merge conflicts
NickSulistio May 25, 2021
dc63d37
fixed bug where tracer was being unused
NickSulistio May 25, 2021
738065c
Merge branch 'main' into flask_hooks
NickSulistio May 26, 2021
a520dfa
removed venv
NickSulistio May 26, 2021
a308044
Merge branch 'flask_hooks' of https://github.com/nicksulistio/opentel…
NickSulistio May 26, 2021
2757171
added if callable check
NickSulistio May 26, 2021
327905e
Merge branch 'main' into flask_hooks
lzchen May 26, 2021
6dd503c
Update instrumentation/opentelemetry-instrumentation-flask/src/opente…
NickSulistio May 27, 2021
2f2611e
removed useless comment
NickSulistio May 27, 2021
e941831
Merge branch 'main' into flask_hooks
lzchen Jun 1, 2021
34e7013
tried linting
NickSulistio Jun 1, 2021
5534ed1
Merge branch 'main' of https://github.com/open-telemetry/opentelemetr…
NickSulistio Jun 1, 2021
b867ead
linted and merged
NickSulistio Jun 1, 2021
ddc10d7
Merge branch 'flask_hooks' of https://github.com/nicksulistio/opentel…
NickSulistio Jun 1, 2021
cbc0995
linting and merge
NickSulistio Jun 1, 2021
d4eab75
removed venv
NickSulistio Jun 1, 2021
422d67d
removed venv
NickSulistio Jun 1, 2021
dafb554
Merge branch 'flask_hooks' of https://github.com/nicksulistio/opentel…
NickSulistio Jun 1, 2021
d25c675
Revert "linting and merge"
NickSulistio Jun 1, 2021
e91fb0b
Revert "Merge branch 'flask_hooks' of https://github.com/nicksulistio…
NickSulistio Jun 1, 2021
aa37da5
Revert "Revert "Merge branch 'flask_hooks' of https://github.com/nick…
NickSulistio Jun 1, 2021
7b017ec
Revert "Revert "linting and merge""
NickSulistio Jun 1, 2021
add1367
reverted
NickSulistio Jun 1, 2021
a8f353e
Revert "tried linting"
NickSulistio Jun 1, 2021
1aafe2b
Revert "tried linting"
NickSulistio Jun 1, 2021
e95313f
Merge branch 'flask_hooks' of https://github.com/nicksulistio/opentel…
NickSulistio Jun 1, 2021
ff10d2b
reverted to d4eab7503c
NickSulistio Jun 1, 2021
52cfe53
fixed generate
NickSulistio Jun 1, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
([#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.
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
- `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def hello():
from opentelemetry.util._time import _time_ns
from opentelemetry.util.http import get_excluded_urls


_logger = getLogger(__name__)

_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
Expand All @@ -82,8 +83,7 @@ def get_default_span_name():
span_name = otel_wsgi.get_default_span_name(flask.request.environ)
return span_name


def _rewrapped_app(wsgi_app):
def _rewrapped_app(wsgi_app, response_hook=None):
def _wrapped_app(wrapped_app_environ, start_response):
# We want to measure the time for route matching, etc.
# In theory, we could start the span here and use
Expand Down Expand Up @@ -112,21 +112,23 @@ def _start_response(status, response_headers, *args, **kwargs):
"missing at _start_response(%s)",
status,
)

if response_hook:
NickSulistio marked this conversation as resolved.
Show resolved Hide resolved
response_hook(span, status, response_headers)
return start_response(status, response_headers, *args, **kwargs)

return wsgi_app(wrapped_app_environ, _start_response)

return _wrapped_app


def _wrapped_before_request(name_callback, tracer):
def _wrapped_before_request(request_hook=None, tracer=None):

def _before_request():
if _excluded_urls.url_disabled(flask.request.url):
return

flask_request_environ = flask.request.environ
span_name = name_callback()
span_name = get_default_span_name()
token = context.attach(
extract(flask_request_environ, getter=otel_wsgi.wsgi_getter)
)
Expand All @@ -136,6 +138,9 @@ def _before_request():
kind=trace.SpanKind.SERVER,
start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
)
if request_hook:
request_hook(span, flask_request_environ)

if span.is_recording():
attributes = otel_wsgi.collect_request_attributes(
flask_request_environ
Expand All @@ -154,7 +159,8 @@ def _before_request():
flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
flask_request_environ[_ENVIRON_SPAN_KEY] = span
flask_request_environ[_ENVIRON_TOKEN] = token



return _before_request


Expand All @@ -181,21 +187,23 @@ def _teardown_request(exc):

class _InstrumentedFlask(flask.Flask):

name_callback = get_default_span_name
_tracer_provider = None

_request_hook = None
_response_hook = None

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

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(
__name__, __version__, _InstrumentedFlask._tracer_provider
)

_before_request = _wrapped_before_request(
_InstrumentedFlask.name_callback, tracer,
_InstrumentedFlask._request_hook, tracer,
)
self._before_request = _before_request
self.before_request(_before_request)
Expand All @@ -208,29 +216,32 @@ class FlaskInstrumentor(BaseInstrumentor):

See `BaseInstrumentor`
"""

def _instrument(self, **kwargs):
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")
if callable(name_callback):
_InstrumentedFlask.name_callback = name_callback
_InstrumentedFlask._tracer_provider = tracer_provider
flask.Flask = _InstrumentedFlask

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
if not hasattr(app, "_is_instrumented"):
app._is_instrumented = False

if not app._is_instrumented:
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)

_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.teardown_request(_teardown_request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ def test_trace_response(self):

self.assertIn("traceresponse", headers)
self.assertEqual(
headers["access-control-expose-headers"], "traceresponse",
headers["access-control-expose-headers"],
"traceresponse",
)
self.assertEqual(
headers["traceresponse"],
Expand Down Expand Up @@ -184,7 +185,7 @@ def test_404(self):
self.assertEqual(span_list[0].name, "HTTP POST")
self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
self.assertEqual(span_list[0].attributes, expected_attrs)

def test_internal_error(self):
expected_attrs = expected_attributes(
{
Expand Down Expand Up @@ -220,19 +221,28 @@ def test_exclude_lists(self):
self.assertEqual(len(span_list), 1)


class TestProgrammaticCustomSpanName(
InstrumentationTest, TestBase, WsgiTestBase
):
class TestProgrammaticHooks(InstrumentationTest, TestBase, WsgiTestBase):
def setUp(self):
super().setUp()

def custom_span_name():
return "flask-custom-span-name"
hook_headers = (
"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__)

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()
Expand All @@ -242,24 +252,45 @@ def tearDown(self):
with self.disable_logging():
FlaskInstrumentor().uninstrument_app(self.app)

def test_custom_span_name(self):
self.client.get("/hello/123")
def test_hooks(self):
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()
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
):
def setUp(self):
super().setUp()

def custom_span_name():
return "instrument-without-app"
hook_headers = (
"hook_attr",
"hello otel without app",
)

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")
# environ.headers.set("apple", "cat")
NickSulistio marked this conversation as resolved.
Show resolved Hide resolved
response_headers.append(hook_headers)

FlaskInstrumentor().instrument(name_callback=custom_span_name)
FlaskInstrumentor().instrument(
request_hook=request_hook_test, response_hook=response_hook_test
)
# pylint: disable=import-outside-toplevel,reimported,redefined-outer-name
from flask import Flask

Expand All @@ -272,12 +303,21 @@ def tearDown(self):
with self.disable_logging():
FlaskInstrumentor().uninstrument()

def test_custom_span_name(self):
self.client.get("/hello/123")
def test_no_app_hooks(self):
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()
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(
Expand Down