Skip to content

Commit

Permalink
GraphQL Server Testing (#506)
Browse files Browse the repository at this point in the history
* Finalize Sanic testing

* Fix flask framework details with callable

* Parametrized testing for graphql-server

* Add middleware tests to graphqlserver

* Reenable code coverage

* [Mega-Linter] Apply linters fixes

* Bump Tests

* Fix nonlocal binding issues in python 2

Co-authored-by: TimPansino <TimPansino@users.noreply.github.com>
  • Loading branch information
TimPansino and TimPansino authored Mar 31, 2022
1 parent 191f2e8 commit 367e57b
Show file tree
Hide file tree
Showing 6 changed files with 624 additions and 164 deletions.
164 changes: 81 additions & 83 deletions newrelic/api/wsgi_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import logging
import sys
import time
import logging
import functools

from newrelic.api.application import application_instance
from newrelic.api.transaction import current_transaction
from newrelic.api.time_trace import notice_error
from newrelic.api.web_transaction import WSGIWebTransaction
from newrelic.api.function_trace import FunctionTrace
from newrelic.api.html_insertion import insert_html_snippet, verify_body_exists

from newrelic.api.time_trace import notice_error
from newrelic.api.transaction import current_transaction
from newrelic.api.web_transaction import WSGIWebTransaction
from newrelic.common.object_names import callable_name
from newrelic.common.object_wrapper import wrap_object, FunctionWrapper

from newrelic.common.object_wrapper import FunctionWrapper, wrap_object
from newrelic.packages import six

_logger = logging.getLogger(__name__)


class _WSGIApplicationIterable(object):

def __init__(self, transaction, generator):
self.transaction = transaction
self.generator = generator
Expand Down Expand Up @@ -68,8 +65,7 @@ def start_trace(self):
self.transaction._sent_start = time.time()

if not self.response_trace:
self.response_trace = FunctionTrace(
name='Response', group='Python/WSGI')
self.response_trace = FunctionTrace(name="Response", group="Python/WSGI")
self.response_trace.__enter__()

def close(self):
Expand All @@ -81,13 +77,12 @@ def close(self):
self.response_trace = None

try:
with FunctionTrace(
name='Finalize', group='Python/WSGI'):
with FunctionTrace(name="Finalize", group="Python/WSGI"):

if isinstance(self.generator, _WSGIApplicationMiddleware):
self.generator.close()

elif hasattr(self.generator, 'close'):
elif hasattr(self.generator, "close"):
name = callable_name(self.generator.close)
with FunctionTrace(name):
self.generator.close()
Expand All @@ -105,7 +100,6 @@ def close(self):


class _WSGIInputWrapper(object):

def __init__(self, transaction, input):
self.__transaction = transaction
self.__input = input
Expand All @@ -114,7 +108,7 @@ def __getattr__(self, name):
return getattr(self.__input, name)

def close(self):
if hasattr(self.__input, 'close'):
if hasattr(self.__input, "close"):
self.__input.close()

def read(self, *args, **kwargs):
Expand Down Expand Up @@ -204,8 +198,7 @@ def __init__(self, application, environ, start_response, transaction):

# Grab the iterable returned by the wrapped WSGI
# application.
self.iterable = self.application(self.request_environ,
self.start_response)
self.iterable = self.application(self.request_environ, self.start_response)

def process_data(self, data):
# If this is the first data block, then immediately try
Expand All @@ -217,7 +210,7 @@ def html_to_be_inserted():
header = self.transaction.browser_timing_header()

if not header:
return b''
return b""

footer = self.transaction.browser_timing_footer()

Expand All @@ -228,10 +221,12 @@ def html_to_be_inserted():

if modified is not None:
if self.debug:
_logger.debug('RUM insertion from WSGI middleware '
'triggered on first yielded string from '
'response. Bytes added was %r.',
len(modified) - len(data))
_logger.debug(
"RUM insertion from WSGI middleware "
"triggered on first yielded string from "
"response. Bytes added was %r.",
len(modified) - len(data),
)

if self.content_length is not None:
length = len(modified) - len(data)
Expand Down Expand Up @@ -264,7 +259,7 @@ def html_to_be_inserted():

if self.response_data:
self.response_data.append(data)
data = b''.join(self.response_data)
data = b"".join(self.response_data)
self.response_data = []

# Perform the insertion of the HTML. This should always
Expand All @@ -276,10 +271,12 @@ def html_to_be_inserted():

if modified is not None:
if self.debug:
_logger.debug('RUM insertion from WSGI middleware '
'triggered on subsequent string yielded from '
'response. Bytes added was %r.',
len(modified) - len(data))
_logger.debug(
"RUM insertion from WSGI middleware "
"triggered on subsequent string yielded from "
"response. Bytes added was %r.",
len(modified) - len(data),
)

if self.content_length is not None:
length = len(modified) - len(data)
Expand All @@ -297,11 +294,10 @@ def flush_headers(self):
# additional data was inserted into the response.

if self.content_length is not None:
header = (('Content-Length', str(self.content_length)))
header = ("Content-Length", str(self.content_length))
self.response_headers.append(header)

self.outer_write = self.outer_start_response(self.response_status,
self.response_headers, *self.response_args)
self.outer_write = self.outer_start_response(self.response_status, self.response_headers, *self.response_args)

def inner_write(self, data):
# If the write() callable is used, we do not attempt to
Expand Down Expand Up @@ -345,8 +341,7 @@ def start_response(self, status, response_headers, *args):
# This is because it can be disabled using an API call.
# Also check whether RUM insertion has already occurred.

if (self.transaction.autorum_disabled or
self.transaction.rum_header_generated):
if self.transaction.autorum_disabled or self.transaction.rum_header_generated:

self.flush_headers()
self.pass_through = True
Expand All @@ -370,21 +365,21 @@ def start_response(self, status, response_headers, *args):
for (name, value) in response_headers:
_name = name.lower()

if _name == 'content-length':
if _name == "content-length":
try:
content_length = int(value)
continue

except ValueError:
pass_through = True

elif _name == 'content-type':
elif _name == "content-type":
content_type = value

elif _name == 'content-encoding':
elif _name == "content-encoding":
content_encoding = value

elif _name == 'content-disposition':
elif _name == "content-disposition":
content_disposition = value

headers.append((name, value))
Expand All @@ -408,9 +403,7 @@ def should_insert_html():

return False

if (content_disposition is not None and
content_disposition.split(';')[0].strip().lower() ==
'attachment'):
if content_disposition is not None and content_disposition.split(";")[0].strip().lower() == "attachment":
return False

if content_type is None:
Expand All @@ -419,7 +412,7 @@ def should_insert_html():
settings = self.transaction.settings
allowed_content_type = settings.browser_monitoring.content_type

if content_type.split(';')[0] not in allowed_content_type:
if content_type.split(";")[0] not in allowed_content_type:
return False

return True
Expand All @@ -443,7 +436,7 @@ def close(self):
# Call close() on the iterable as required by the
# WSGI specification.

if hasattr(self.iterable, 'close'):
if hasattr(self.iterable, "close"):
name = callable_name(self.iterable.close)
with FunctionTrace(name):
self.iterable.close()
Expand Down Expand Up @@ -518,18 +511,35 @@ def __iter__(self):
yield data


def WSGIApplicationWrapper(wrapped, application=None, name=None,
group=None, framework=None):
def WSGIApplicationWrapper(wrapped, application=None, name=None, group=None, framework=None):

# Python 2 does not allow rebinding nonlocal variables, so to fix this
# framework must be stored in list so it can be edited by closure.
_framework = [framework]

def get_framework():
"""Used to delay imports by passing framework as a callable."""
framework = _framework[0]
if isinstance(framework, tuple) or framework is None:
return framework

if callable(framework):
framework = framework()
_framework[0] = framework

if framework is not None and not isinstance(framework, tuple):
framework = (framework, None)
_framework[0] = framework

if framework is not None and not isinstance(framework, tuple):
framework = (framework, None)
return framework

def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs):
# Check to see if any transaction is present, even an inactive
# one which has been marked to be ignored or which has been
# stopped already.

transaction = current_transaction(active_only=False)
framework = get_framework()

if transaction:
# If there is any active transaction we will return without
Expand All @@ -545,8 +555,7 @@ def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs):
# supportability metrics.

if framework:
transaction.add_framework_info(
name=framework[0], version=framework[1])
transaction.add_framework_info(name=framework[0], version=framework[1])

# Also override the web transaction name to be the name of
# the wrapped callable if not explicitly named, and we want
Expand All @@ -560,9 +569,8 @@ def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs):
if name is None and settings:
if framework is not None:
naming_scheme = settings.transaction_name.naming_scheme
if naming_scheme in (None, 'framework'):
transaction.set_transaction_name(
callable_name(wrapped), priority=1)
if naming_scheme in (None, "framework"):
transaction.set_transaction_name(callable_name(wrapped), priority=1)

elif name:
transaction.set_transaction_name(name, group, priority=1)
Expand All @@ -580,11 +588,11 @@ def _args(environ, start_response, *args, **kwargs):

target_application = application

if 'newrelic.app_name' in environ:
app_name = environ['newrelic.app_name']
if "newrelic.app_name" in environ:
app_name = environ["newrelic.app_name"]

if ';' in app_name:
app_names = [n.strip() for n in app_name.split(';')]
if ";" in app_name:
app_names = [n.strip() for n in app_name.split(";")]
app_name = app_names[0]
target_application = application_instance(app_name)
for altname in app_names[1:]:
Expand All @@ -598,7 +606,7 @@ def _args(environ, start_response, *args, **kwargs):

# FIXME Should this allow for multiple apps if a string.

if not hasattr(application, 'activate'):
if not hasattr(application, "activate"):
target_application = application_instance(application)

# Now start recording the actual web transaction.
Expand All @@ -609,8 +617,7 @@ def _args(environ, start_response, *args, **kwargs):
# reporting as supportability metrics.

if framework:
transaction.add_framework_info(
name=framework[0], version=framework[1])
transaction.add_framework_info(name=framework[0], version=framework[1])

# Override the initial web transaction name to be the supplied
# name, or the name of the wrapped callable if wanting to use
Expand All @@ -630,24 +637,20 @@ def _args(environ, start_response, *args, **kwargs):
naming_scheme = settings.transaction_name.naming_scheme

if framework is not None:
if naming_scheme in (None, 'framework'):
transaction.set_transaction_name(
callable_name(wrapped), priority=1)
if naming_scheme in (None, "framework"):
transaction.set_transaction_name(callable_name(wrapped), priority=1)

elif naming_scheme in ('component', 'framework'):
transaction.set_transaction_name(
callable_name(wrapped), priority=1)
elif naming_scheme in ("component", "framework"):
transaction.set_transaction_name(callable_name(wrapped), priority=1)

elif name:
transaction.set_transaction_name(name, group, priority=1)

def _start_response(status, response_headers, *args):

additional_headers = transaction.process_response(
status, response_headers, *args)
additional_headers = transaction.process_response(status, response_headers, *args)

_write = start_response(status,
response_headers + additional_headers, *args)
_write = start_response(status, response_headers + additional_headers, *args)

def write(data):
if not transaction._sent_start:
Expand All @@ -667,17 +670,13 @@ def write(data):
# Should always exist, but check as test harnesses may not
# have it.

if 'wsgi.input' in environ:
environ['wsgi.input'] = _WSGIInputWrapper(transaction,
environ['wsgi.input'])
if "wsgi.input" in environ:
environ["wsgi.input"] = _WSGIInputWrapper(transaction, environ["wsgi.input"])

with FunctionTrace(
name='Application', group='Python/WSGI'):
with FunctionTrace(name="Application", group="Python/WSGI"):
with FunctionTrace(name=callable_name(wrapped)):
if (settings and settings.browser_monitoring.enabled and
not transaction.autorum_disabled):
result = _WSGIApplicationMiddleware(wrapped,
environ, _start_response, transaction)
if settings and settings.browser_monitoring.enabled and not transaction.autorum_disabled:
result = _WSGIApplicationMiddleware(wrapped, environ, _start_response, transaction)
else:
result = wrapped(environ, _start_response)

Expand All @@ -691,11 +690,10 @@ def write(data):


def wsgi_application(application=None, name=None, group=None, framework=None):
return functools.partial(WSGIApplicationWrapper, application=application,
name=name, group=group, framework=framework)
return functools.partial(
WSGIApplicationWrapper, application=application, name=name, group=group, framework=framework
)


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):
wrap_object(module, object_path, WSGIApplicationWrapper, (application, name, group, framework))
Loading

0 comments on commit 367e57b

Please sign in to comment.