diff --git a/newrelic/api/asgi_application.py b/newrelic/api/asgi_application.py index 6ba049d9eb..2e4e4979b3 100644 --- a/newrelic/api/asgi_application.py +++ b/newrelic/api/asgi_application.py @@ -371,20 +371,23 @@ async def nr_async_asgi(receive, send): return FunctionWrapper(wrapped, nr_asgi_wrapper) -def asgi_application(application=None, name=None, group=None, framework=None): +def asgi_application(application=None, name=None, group=None, framework=None, dispatcher=None): return functools.partial( ASGIApplicationWrapper, application=application, name=name, group=group, framework=framework, + dispatcher=dispatcher, ) -def wrap_asgi_application(module, object_path, application=None, name=None, group=None, framework=None): +def wrap_asgi_application( + module, object_path, application=None, name=None, group=None, framework=None, dispatcher=None +): wrap_object( module, object_path, ASGIApplicationWrapper, - (application, name, group, framework), + (application, name, group, framework, dispatcher), ) diff --git a/newrelic/api/wsgi_application.py b/newrelic/api/wsgi_application.py index 502c9d8a03..67338cbddd 100644 --- a/newrelic/api/wsgi_application.py +++ b/newrelic/api/wsgi_application.py @@ -691,11 +691,18 @@ def write(data): return FunctionWrapper(wrapped, _nr_wsgi_application_wrapper_) -def wsgi_application(application=None, name=None, group=None, framework=None): +def wsgi_application(application=None, name=None, group=None, framework=None, dispatcher=None): return functools.partial( - WSGIApplicationWrapper, application=application, name=name, group=group, framework=framework + WSGIApplicationWrapper, + application=application, + name=name, + group=group, + framework=framework, + dispatcher=dispatcher, ) -def wrap_wsgi_application(module, object_path, application=None, name=None, group=None, framework=None): - wrap_object(module, object_path, WSGIApplicationWrapper, (application, name, group, framework)) +def wrap_wsgi_application( + module, object_path, application=None, name=None, group=None, framework=None, dispatcher=None +): + wrap_object(module, object_path, WSGIApplicationWrapper, (application, name, group, framework, dispatcher)) diff --git a/tests/adapter_daphne/test_daphne.py b/tests/adapter_daphne/test_daphne.py index 80faac992b..471e0335b8 100644 --- a/tests/adapter_daphne/test_daphne.py +++ b/tests/adapter_daphne/test_daphne.py @@ -29,6 +29,7 @@ AppWithCall, AppWithCallRaw, simple_app_v2_raw, + simple_app_v3, ) from testing_support.util import get_open_port @@ -45,6 +46,10 @@ simple_app_v2_raw, marks=skip_asgi_2_unsupported, ), + pytest.param( + simple_app_v3, + marks=skip_asgi_3_unsupported, + ), pytest.param( AppWithCallRaw(), marks=skip_asgi_3_unsupported, @@ -54,7 +59,7 @@ marks=skip_asgi_3_unsupported, ), ), - ids=("raw", "class_with_call", "class_with_call_double_wrapped"), + ids=("raw", "wrapped", "class_with_call", "class_with_call_double_wrapped"), ) def app(request, server_and_port): app = request.param diff --git a/tests/agent_features/test_asgi_transaction.py b/tests/agent_features/test_asgi_transaction.py index 1820efa86d..520e954bc7 100644 --- a/tests/agent_features/test_asgi_transaction.py +++ b/tests/agent_features/test_asgi_transaction.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import asyncio import logging import pytest @@ -124,15 +123,34 @@ def test_asgi_application_decorator_no_params_double_callable(): assert response.body == b"" -# Test for presence of framework info based on whether framework is specified -@validate_transaction_metrics(name="test", custom_metrics=[("Python/Framework/framework/v1", 1)]) -def test_framework_metrics(): - asgi_decorator = asgi_application(name="test", framework=("framework", "v1")) +# Test for presence of framework and dispatcher info based on whether framework is specified +@validate_transaction_metrics( + name="test", custom_metrics=[("Python/Framework/framework/v1", 1), ("Python/Dispatcher/dispatcher/v1.0.0", 1)] +) +def test_dispatcher_and_framework_metrics(): + asgi_decorator = asgi_application(name="test", framework=("framework", "v1"), dispatcher=("dispatcher", "v1.0.0")) decorated_application = asgi_decorator(simple_app_v2_raw) application = AsgiTest(decorated_application) application.make_request("GET", "/") +# Test for presence of framework and dispatcher info under existing transaction +@validate_transaction_metrics( + name="test", custom_metrics=[("Python/Framework/framework/v1", 1), ("Python/Dispatcher/dispatcher/v1.0.0", 1)] +) +def test_double_wrapped_dispatcher_and_framework_metrics(): + inner_asgi_decorator = asgi_application( + name="test", framework=("framework", "v1"), dispatcher=("dispatcher", "v1.0.0") + ) + decorated_application = inner_asgi_decorator(simple_app_v2_raw) + + outer_asgi_decorator = asgi_application(name="double_wrapped") + double_decorated_application = outer_asgi_decorator(decorated_application) + + application = AsgiTest(double_decorated_application) + application.make_request("GET", "/") + + @pytest.mark.parametrize("method", ("method", "cls", "static")) @validate_transaction_metrics(name="", group="Uri") def test_app_with_descriptor(method): diff --git a/tests/agent_features/test_web_transaction.py b/tests/agent_features/test_web_transaction.py index 22bfd6eb8e..0d8b175484 100644 --- a/tests/agent_features/test_web_transaction.py +++ b/tests/agent_features/test_web_transaction.py @@ -14,67 +14,109 @@ # limitations under the License. import gc -import webtest -import pytest import time + +import pytest +import webtest +from testing_support.fixtures import validate_attributes, validate_transaction_metrics +from testing_support.sample_applications import simple_app, simple_app_raw + +import newrelic.packages.six as six from newrelic.api.application import application_instance from newrelic.api.web_transaction import WebTransaction -from testing_support.fixtures import (validate_transaction_metrics, - validate_attributes) -from testing_support.sample_applications import simple_app -import newrelic.packages.six as six +from newrelic.api.wsgi_application import wsgi_application + application = webtest.TestApp(simple_app) # TODO: WSGI metrics must not be generated for a WebTransaction METRICS = ( - ('Python/WSGI/Input/Bytes', None), - ('Python/WSGI/Input/Time', None), - ('Python/WSGI/Input/Calls/read', None), - ('Python/WSGI/Input/Calls/readline', None), - ('Python/WSGI/Input/Calls/readlines', None), - ('Python/WSGI/Output/Bytes', None), - ('Python/WSGI/Output/Time', None), - ('Python/WSGI/Output/Calls/yield', None), - ('Python/WSGI/Output/Calls/write', None), + ("Python/WSGI/Input/Bytes", None), + ("Python/WSGI/Input/Time", None), + ("Python/WSGI/Input/Calls/read", None), + ("Python/WSGI/Input/Calls/readline", None), + ("Python/WSGI/Input/Calls/readlines", None), + ("Python/WSGI/Output/Bytes", None), + ("Python/WSGI/Output/Time", None), + ("Python/WSGI/Output/Calls/yield", None), + ("Python/WSGI/Output/Calls/write", None), ) -# TODO: Add rollup_metrics=METRICS +# Test for presence of framework and dispatcher info based on whether framework is specified +@validate_transaction_metrics( + name="test", custom_metrics=[("Python/Framework/framework/v1", 1), ("Python/Dispatcher/dispatcher/v1.0.0", 1)] +) +def test_dispatcher_and_framework_metrics(): + inner_wsgi_decorator = wsgi_application( + name="test", framework=("framework", "v1"), dispatcher=("dispatcher", "v1.0.0") + ) + decorated_application = inner_wsgi_decorator(simple_app_raw) + + application = webtest.TestApp(decorated_application) + application.get("/") + + +# Test for presence of framework and dispatcher info under existing transaction @validate_transaction_metrics( - 'test_base_web_transaction', - group='Test') -@validate_attributes('agent', -[ - 'request.headers.accept', 'request.headers.contentLength', - 'request.headers.contentType', 'request.headers.host', - 'request.headers.referer', 'request.headers.userAgent', 'request.method', - 'request.uri', 'response.status', 'response.headers.contentLength', - 'response.headers.contentType', 'request.parameters.foo', - 'request.parameters.boo', 'webfrontend.queue.seconds', -]) -@pytest.mark.parametrize('use_bytes', (True, False)) + name="test", custom_metrics=[("Python/Framework/framework/v1", 1), ("Python/Dispatcher/dispatcher/v1.0.0", 1)] +) +def test_double_wrapped_dispatcher_and_framework_metrics(): + inner_wsgi_decorator = wsgi_application( + name="test", framework=("framework", "v1"), dispatcher=("dispatcher", "v1.0.0") + ) + decorated_application = inner_wsgi_decorator(simple_app_raw) + + outer_wsgi_decorator = wsgi_application(name="double_wrapped") + double_decorated_application = outer_wsgi_decorator(decorated_application) + + application = webtest.TestApp(double_decorated_application) + application.get("/") + + +# TODO: Add rollup_metrics=METRICS +@validate_transaction_metrics("test_base_web_transaction", group="Test") +@validate_attributes( + "agent", + [ + "request.headers.accept", + "request.headers.contentLength", + "request.headers.contentType", + "request.headers.host", + "request.headers.referer", + "request.headers.userAgent", + "request.method", + "request.uri", + "response.status", + "response.headers.contentLength", + "response.headers.contentType", + "request.parameters.foo", + "request.parameters.boo", + "webfrontend.queue.seconds", + ], +) +@pytest.mark.parametrize("use_bytes", (True, False)) def test_base_web_transaction(use_bytes): application = application_instance() request_headers = { - 'Accept': 'text/plain', - 'Content-Length': '0', - 'Content-Type': 'text/plain', - 'Host': 'localhost', - 'Referer': 'http://example.com?q=1&boat=⛵', - 'User-Agent': 'potato', - 'X-Request-Start': str(time.time() - 0.2), - 'newRelic': 'invalid', + "Accept": "text/plain", + "Content-Length": "0", + "Content-Type": "text/plain", + "Host": "localhost", + "Referer": "http://example.com?q=1&boat=⛵", + "User-Agent": "potato", + "X-Request-Start": str(time.time() - 0.2), + "newRelic": "invalid", } if use_bytes: byte_headers = {} for name, value in request_headers.items(): - name = name.encode('utf-8') + name = name.encode("utf-8") try: - value = value.encode('utf-8') + value = value.encode("utf-8") except UnicodeDecodeError: assert six.PY2 byte_headers[name] = value @@ -82,24 +124,22 @@ def test_base_web_transaction(use_bytes): request_headers = byte_headers transaction = WebTransaction( - application, - 'test_base_web_transaction', - group='Test', - scheme='http', - host='localhost', - port=8000, - request_method='HEAD', - request_path='/foobar', - query_string='foo=bar&boo=baz', - headers=request_headers.items(), + application, + "test_base_web_transaction", + group="Test", + scheme="http", + host="localhost", + port=8000, + request_method="HEAD", + request_path="/foobar", + query_string="foo=bar&boo=baz", + headers=request_headers.items(), ) if use_bytes: - response_headers = ((b'Content-Length', b'0'), - (b'Content-Type', b'text/plain')) + response_headers = ((b"Content-Length", b"0"), (b"Content-Type", b"text/plain")) else: - response_headers = (('Content-Length', '0'), - ('Content-Type', 'text/plain')) + response_headers = (("Content-Length", "0"), ("Content-Type", "text/plain")) with transaction: transaction.process_response(200, response_headers) @@ -117,8 +157,8 @@ def validate_no_garbage(): @validate_transaction_metrics( - name='', - group='Uri', + name="", + group="Uri", ) def test_wsgi_app_memory(validate_no_garbage): - application.get('/') + application.get("/") diff --git a/tests/testing_support/sample_applications.py b/tests/testing_support/sample_applications.py index 74d5e6dbec..7973a4e11c 100644 --- a/tests/testing_support/sample_applications.py +++ b/tests/testing_support/sample_applications.py @@ -128,8 +128,7 @@ def simple_exceptional_app(environ, start_response): raise ValueError("Transaction had bad value") -@wsgi_application() -def simple_app(environ, start_response): +def simple_app_raw(environ, start_response): status = "200 OK" _logger.info("Starting response") @@ -138,6 +137,9 @@ def simple_app(environ, start_response): return [] +simple_app = wsgi_application()(simple_app_raw) + + @wsgi_application() def simple_custom_event_app(environ, start_response):