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

Fix Flask view support in Code Level Metrics #664

Merged
merged 7 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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