Skip to content

Commit c96ffc5

Browse files
TimPansinolrafeeihmstepanekumaannamalai
authored
Fix Flask view support in Code Level Metrics (#664)
* Fix Flask view support in Code Level Metrics Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> * [Mega-Linter] Apply linters fixes * Bump tests * Fix CLM tests for flaskrest * [Mega-Linter] Apply linters fixes * Bump tests Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: TimPansino <[email protected]> Co-authored-by: Uma Annamalai <[email protected]>
1 parent 922db2f commit c96ffc5

File tree

3 files changed

+67
-38
lines changed

3 files changed

+67
-38
lines changed

newrelic/hooks/framework_flask.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
1717
"""
1818

19+
from inspect import isclass
20+
1921
from newrelic.api.function_trace import (
2022
FunctionTrace,
2123
FunctionTraceWrapper,
@@ -55,6 +57,22 @@ def _nr_wrapper_handler_(wrapped, instance, args, kwargs):
5557
name = getattr(wrapped, "_nr_view_func_name", callable_name(wrapped))
5658
view = getattr(wrapped, "view_class", wrapped)
5759

60+
try:
61+
# Attempt to narrow down class based views to the correct method
62+
from flask import request
63+
from flask.views import MethodView
64+
65+
if isclass(view):
66+
if issubclass(view, MethodView):
67+
# For method based views, use the corresponding method if available
68+
method = request.method.lower()
69+
view = getattr(view, method, view)
70+
else:
71+
# For class based views, use the dispatch_request function if available
72+
view = getattr(view, "dispatch_request", view)
73+
except ImportError:
74+
pass
75+
5876
# Set priority=2 so this will take precedence over any error
5977
# handler which will be at priority=1.
6078

tests/component_flask_rest/test_application.py

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,27 @@
1313
# limitations under the License.
1414

1515
import pytest
16+
from testing_support.fixtures import (
17+
override_generic_settings,
18+
override_ignore_status_codes,
19+
validate_transaction_errors,
20+
validate_transaction_metrics,
21+
)
22+
from testing_support.validators.validate_code_level_metrics import (
23+
validate_code_level_metrics,
24+
)
1625

26+
from newrelic.common.object_names import callable_name
27+
from newrelic.core.config import global_settings
1728
from newrelic.packages import six
18-
from testing_support.fixtures import (validate_transaction_metrics,
19-
validate_transaction_errors, override_ignore_status_codes,
20-
override_generic_settings)
21-
from testing_support.validators.validate_code_level_metrics import validate_code_level_metrics
2229

23-
from newrelic.core.config import global_settings
24-
from newrelic.common.object_names import callable_name
30+
TEST_APPLICATION_PREFIX = "_test_application.create_app.<locals>" if six.PY3 else "_test_application"
2531

2632

2733
@pytest.fixture(params=["flask_restful", "flask_restplus", "flask_restx"])
2834
def application(request):
2935
from _test_application import get_test_application
36+
3037
if request.param == "flask_restful":
3138
import flask_restful as module
3239
elif request.param == "flask_restplus":
@@ -44,49 +51,46 @@ def application(request):
4451

4552

4653
_test_application_index_scoped_metrics = [
47-
('Function/flask.app:Flask.wsgi_app', 1),
48-
('Python/WSGI/Application', 1),
49-
('Python/WSGI/Response', 1),
50-
('Python/WSGI/Finalize', 1),
51-
('Function/_test_application:index', 1),
52-
('Function/werkzeug.wsgi:ClosingIterator.close', 1),
54+
("Function/flask.app:Flask.wsgi_app", 1),
55+
("Python/WSGI/Application", 1),
56+
("Python/WSGI/Response", 1),
57+
("Python/WSGI/Finalize", 1),
58+
("Function/_test_application:index", 1),
59+
("Function/werkzeug.wsgi:ClosingIterator.close", 1),
5360
]
5461

5562

56-
@validate_code_level_metrics("_test_application.create_app.<locals>" if six.PY3 else "_test_application", "IndexResource")
63+
@validate_code_level_metrics(TEST_APPLICATION_PREFIX + ".IndexResource", "get")
5764
@validate_transaction_errors(errors=[])
58-
@validate_transaction_metrics('_test_application:index',
59-
scoped_metrics=_test_application_index_scoped_metrics)
65+
@validate_transaction_metrics("_test_application:index", scoped_metrics=_test_application_index_scoped_metrics)
6066
def test_application_index(application):
61-
response = application.get('/index')
62-
response.mustcontain('hello')
67+
response = application.get("/index")
68+
response.mustcontain("hello")
6369

6470

6571
_test_application_raises_scoped_metrics = [
66-
('Function/flask.app:Flask.wsgi_app', 1),
67-
('Python/WSGI/Application', 1),
68-
('Function/_test_application:exception', 1),
72+
("Function/flask.app:Flask.wsgi_app", 1),
73+
("Python/WSGI/Application", 1),
74+
("Function/_test_application:exception", 1),
6975
]
7076

7177

7278
@pytest.mark.parametrize(
73-
'exception,status_code,ignore_status_code,propagate_exceptions', [
74-
('werkzeug.exceptions:HTTPException', 404, False, False),
75-
('werkzeug.exceptions:HTTPException', 404, True, False),
76-
('werkzeug.exceptions:HTTPException', 503, False, False),
77-
('_test_application:CustomException', 500, False, False),
78-
('_test_application:CustomException', 500, False, True),
79-
])
80-
def test_application_raises(exception, status_code, ignore_status_code,
81-
propagate_exceptions, application):
82-
83-
@validate_code_level_metrics("_test_application.create_app.<locals>" if six.PY3 else "_test_application", "ExceptionResource")
84-
@validate_transaction_metrics('_test_application:exception',
85-
scoped_metrics=_test_application_raises_scoped_metrics)
79+
"exception,status_code,ignore_status_code,propagate_exceptions",
80+
[
81+
("werkzeug.exceptions:HTTPException", 404, False, False),
82+
("werkzeug.exceptions:HTTPException", 404, True, False),
83+
("werkzeug.exceptions:HTTPException", 503, False, False),
84+
("_test_application:CustomException", 500, False, False),
85+
("_test_application:CustomException", 500, False, True),
86+
],
87+
)
88+
def test_application_raises(exception, status_code, ignore_status_code, propagate_exceptions, application):
89+
@validate_code_level_metrics(TEST_APPLICATION_PREFIX + ".ExceptionResource", "get")
90+
@validate_transaction_metrics("_test_application:exception", scoped_metrics=_test_application_raises_scoped_metrics)
8691
def _test():
8792
try:
88-
application.get('/exception/%s/%i' % (exception,
89-
status_code), status=status_code, expect_errors=True)
93+
application.get("/exception/%s/%i" % (exception, status_code), status=status_code, expect_errors=True)
9094
except Exception as e:
9195
assert propagate_exceptions
9296

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

109113
_settings = global_settings()
110114

111-
@override_generic_settings(_settings, {'enabled': False})
115+
@override_generic_settings(_settings, {"enabled": False})
112116
def _test():
113-
application.get('/exception/werkzeug.exceptions:HTTPException/404',
114-
status=404)
117+
application.get("/exception/werkzeug.exceptions:HTTPException/404", status=404)
115118

116119
_test()

tests/framework_flask/test_views.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
validate_transaction_errors,
2121
validate_transaction_metrics,
2222
)
23+
from testing_support.validators.validate_code_level_metrics import (
24+
validate_code_level_metrics,
25+
)
2326

2427
scoped_metrics = [
2528
("Function/flask.app:Flask.wsgi_app", 1),
@@ -50,6 +53,7 @@ def target_application():
5053
return _test_application
5154

5255

56+
@validate_code_level_metrics("_test_views.TestView", "dispatch_request")
5357
@validate_transaction_errors(errors=[])
5458
@validate_transaction_metrics("_test_views:test_view", scoped_metrics=scoped_metrics)
5559
def test_class_based_view():
@@ -59,6 +63,7 @@ def test_class_based_view():
5963

6064

6165
@skip_if_not_async_handler_support
66+
@validate_code_level_metrics("_test_views_async.TestAsyncView", "dispatch_request")
6267
@validate_transaction_errors(errors=[])
6368
@validate_transaction_metrics("_test_views_async:test_async_view", scoped_metrics=scoped_metrics)
6469
def test_class_based_async_view():
@@ -67,6 +72,7 @@ def test_class_based_async_view():
6772
response.mustcontain("ASYNC VIEW RESPONSE")
6873

6974

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

7783

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

8592

8693
@skip_if_not_async_handler_support
94+
@validate_code_level_metrics("_test_views_async.TestAsyncMethodView", "get")
8795
@validate_transaction_errors(errors=[])
8896
@validate_transaction_metrics("_test_views_async:test_async_methodview", scoped_metrics=scoped_metrics)
8997
def test_get_method_async_view():

0 commit comments

Comments
 (0)