Skip to content

Commit

Permalink
Fix Flask view support in Code Level Metrics (#664)
Browse files Browse the repository at this point in the history
* Fix Flask view support in Code Level Metrics

Co-authored-by: Lalleh Rafeei <lrafeei@users.noreply.github.com>
Co-authored-by: Hannah Stepanek <hmstepanek@users.noreply.github.com>
Co-authored-by: Uma Annamalai <umaannamalai@users.noreply.github.com>

* [Mega-Linter] Apply linters fixes

* Bump tests

* Fix CLM tests for flaskrest

* [Mega-Linter] Apply linters fixes

* Bump tests

Co-authored-by: Lalleh Rafeei <lrafeei@users.noreply.github.com>
Co-authored-by: Hannah Stepanek <hmstepanek@users.noreply.github.com>
Co-authored-by: Uma Annamalai <umaannamalai@users.noreply.github.com>
Co-authored-by: TimPansino <TimPansino@users.noreply.github.com>
Co-authored-by: Uma Annamalai <uannamalai@newrelic.com>
  • Loading branch information
6 people authored Oct 24, 2022
1 parent 922db2f commit c96ffc5
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 38 deletions.
18 changes: 18 additions & 0 deletions newrelic/hooks/framework_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"""

from inspect import isclass

from newrelic.api.function_trace import (
FunctionTrace,
FunctionTraceWrapper,
Expand Down Expand Up @@ -55,6 +57,22 @@ def _nr_wrapper_handler_(wrapped, instance, args, kwargs):
name = getattr(wrapped, "_nr_view_func_name", callable_name(wrapped))
view = getattr(wrapped, "view_class", wrapped)

try:
# Attempt to narrow down class based views to the correct method
from flask import request
from flask.views import MethodView

if isclass(view):
if issubclass(view, MethodView):
# For method based views, use the corresponding method if available
method = request.method.lower()
view = getattr(view, method, view)
else:
# For class based views, use the dispatch_request function if available
view = getattr(view, "dispatch_request", view)
except ImportError:
pass

# Set priority=2 so this will take precedence over any error
# handler which will be at priority=1.

Expand Down
79 changes: 41 additions & 38 deletions tests/component_flask_rest/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,27 @@
# limitations under the License.

import pytest
from testing_support.fixtures import (
override_generic_settings,
override_ignore_status_codes,
validate_transaction_errors,
validate_transaction_metrics,
)
from testing_support.validators.validate_code_level_metrics import (
validate_code_level_metrics,
)

from newrelic.common.object_names import callable_name
from newrelic.core.config import global_settings
from newrelic.packages import six
from testing_support.fixtures import (validate_transaction_metrics,
validate_transaction_errors, override_ignore_status_codes,
override_generic_settings)
from testing_support.validators.validate_code_level_metrics import validate_code_level_metrics

from newrelic.core.config import global_settings
from newrelic.common.object_names import callable_name
TEST_APPLICATION_PREFIX = "_test_application.create_app.<locals>" if six.PY3 else "_test_application"


@pytest.fixture(params=["flask_restful", "flask_restplus", "flask_restx"])
def application(request):
from _test_application import get_test_application

if request.param == "flask_restful":
import flask_restful as module
elif request.param == "flask_restplus":
Expand All @@ -44,49 +51,46 @@ def application(request):


_test_application_index_scoped_metrics = [
('Function/flask.app:Flask.wsgi_app', 1),
('Python/WSGI/Application', 1),
('Python/WSGI/Response', 1),
('Python/WSGI/Finalize', 1),
('Function/_test_application:index', 1),
('Function/werkzeug.wsgi:ClosingIterator.close', 1),
("Function/flask.app:Flask.wsgi_app", 1),
("Python/WSGI/Application", 1),
("Python/WSGI/Response", 1),
("Python/WSGI/Finalize", 1),
("Function/_test_application:index", 1),
("Function/werkzeug.wsgi:ClosingIterator.close", 1),
]


@validate_code_level_metrics("_test_application.create_app.<locals>" if six.PY3 else "_test_application", "IndexResource")
@validate_code_level_metrics(TEST_APPLICATION_PREFIX + ".IndexResource", "get")
@validate_transaction_errors(errors=[])
@validate_transaction_metrics('_test_application:index',
scoped_metrics=_test_application_index_scoped_metrics)
@validate_transaction_metrics("_test_application:index", scoped_metrics=_test_application_index_scoped_metrics)
def test_application_index(application):
response = application.get('/index')
response.mustcontain('hello')
response = application.get("/index")
response.mustcontain("hello")


_test_application_raises_scoped_metrics = [
('Function/flask.app:Flask.wsgi_app', 1),
('Python/WSGI/Application', 1),
('Function/_test_application:exception', 1),
("Function/flask.app:Flask.wsgi_app", 1),
("Python/WSGI/Application", 1),
("Function/_test_application:exception", 1),
]


@pytest.mark.parametrize(
'exception,status_code,ignore_status_code,propagate_exceptions', [
('werkzeug.exceptions:HTTPException', 404, False, False),
('werkzeug.exceptions:HTTPException', 404, True, False),
('werkzeug.exceptions:HTTPException', 503, False, False),
('_test_application:CustomException', 500, False, False),
('_test_application:CustomException', 500, False, True),
])
def test_application_raises(exception, status_code, ignore_status_code,
propagate_exceptions, application):

@validate_code_level_metrics("_test_application.create_app.<locals>" if six.PY3 else "_test_application", "ExceptionResource")
@validate_transaction_metrics('_test_application:exception',
scoped_metrics=_test_application_raises_scoped_metrics)
"exception,status_code,ignore_status_code,propagate_exceptions",
[
("werkzeug.exceptions:HTTPException", 404, False, False),
("werkzeug.exceptions:HTTPException", 404, True, False),
("werkzeug.exceptions:HTTPException", 503, False, False),
("_test_application:CustomException", 500, False, False),
("_test_application:CustomException", 500, False, True),
],
)
def test_application_raises(exception, status_code, ignore_status_code, propagate_exceptions, application):
@validate_code_level_metrics(TEST_APPLICATION_PREFIX + ".ExceptionResource", "get")
@validate_transaction_metrics("_test_application:exception", scoped_metrics=_test_application_raises_scoped_metrics)
def _test():
try:
application.get('/exception/%s/%i' % (exception,
status_code), status=status_code, expect_errors=True)
application.get("/exception/%s/%i" % (exception, status_code), status=status_code, expect_errors=True)
except Exception as e:
assert propagate_exceptions

Expand All @@ -108,9 +112,8 @@ def test_application_outside_transaction(application):

_settings = global_settings()

@override_generic_settings(_settings, {'enabled': False})
@override_generic_settings(_settings, {"enabled": False})
def _test():
application.get('/exception/werkzeug.exceptions:HTTPException/404',
status=404)
application.get("/exception/werkzeug.exceptions:HTTPException/404", status=404)

_test()
8 changes: 8 additions & 0 deletions tests/framework_flask/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
validate_transaction_errors,
validate_transaction_metrics,
)
from testing_support.validators.validate_code_level_metrics import (
validate_code_level_metrics,
)

scoped_metrics = [
("Function/flask.app:Flask.wsgi_app", 1),
Expand Down Expand Up @@ -50,6 +53,7 @@ def target_application():
return _test_application


@validate_code_level_metrics("_test_views.TestView", "dispatch_request")
@validate_transaction_errors(errors=[])
@validate_transaction_metrics("_test_views:test_view", scoped_metrics=scoped_metrics)
def test_class_based_view():
Expand All @@ -59,6 +63,7 @@ def test_class_based_view():


@skip_if_not_async_handler_support
@validate_code_level_metrics("_test_views_async.TestAsyncView", "dispatch_request")
@validate_transaction_errors(errors=[])
@validate_transaction_metrics("_test_views_async:test_async_view", scoped_metrics=scoped_metrics)
def test_class_based_async_view():
Expand All @@ -67,6 +72,7 @@ def test_class_based_async_view():
response.mustcontain("ASYNC VIEW RESPONSE")


@validate_code_level_metrics("_test_views.TestMethodView", "get")
@validate_transaction_errors(errors=[])
@validate_transaction_metrics("_test_views:test_methodview", scoped_metrics=scoped_metrics)
def test_get_method_view():
Expand All @@ -75,6 +81,7 @@ def test_get_method_view():
response.mustcontain("METHODVIEW GET RESPONSE")


@validate_code_level_metrics("_test_views.TestMethodView", "post")
@validate_transaction_errors(errors=[])
@validate_transaction_metrics("_test_views:test_methodview", scoped_metrics=scoped_metrics)
def test_post_method_view():
Expand All @@ -84,6 +91,7 @@ def test_post_method_view():


@skip_if_not_async_handler_support
@validate_code_level_metrics("_test_views_async.TestAsyncMethodView", "get")
@validate_transaction_errors(errors=[])
@validate_transaction_metrics("_test_views_async:test_async_methodview", scoped_metrics=scoped_metrics)
def test_get_method_async_view():
Expand Down

0 comments on commit c96ffc5

Please sign in to comment.