Skip to content

Commit

Permalink
Add usage metrics for Daphne and Hypercorn. (#665)
Browse files Browse the repository at this point in the history
* Add usage metrics for Daphne and Hypercorn.

* [Mega-Linter] Apply linters fixes

Co-authored-by: umaannamalai <umaannamalai@users.noreply.github.com>
  • Loading branch information
umaannamalai and umaannamalai authored Oct 20, 2022
1 parent 968f3dc commit 922db2f
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 17 deletions.
10 changes: 7 additions & 3 deletions newrelic/api/asgi_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ async def send(self, event):
return await self._send(event)


def ASGIApplicationWrapper(wrapped, application=None, name=None, group=None, framework=None):
def ASGIApplicationWrapper(wrapped, application=None, name=None, group=None, framework=None, dispatcher=None):
def nr_asgi_wrapper(wrapped, instance, args, kwargs):
double_callable = asgiref_compatibility.is_double_callable(wrapped)
if double_callable:
Expand All @@ -271,9 +271,7 @@ async def nr_async_asgi(receive, send):
# 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)

if transaction:
# If there is any active transaction we will return without
# applying a new ASGI application wrapper context. In the
Expand All @@ -290,6 +288,9 @@ async def nr_async_asgi(receive, send):
if framework:
transaction.add_framework_info(name=framework[0], version=framework[1])

if dispatcher:
transaction.add_dispatcher_info(name=dispatcher[0], version=dispatcher[1])

# Also override the web transaction name to be the name of
# the wrapped callable if not explicitly named, and we want
# the default name to be that of the ASGI component for the
Expand Down Expand Up @@ -323,6 +324,9 @@ async def nr_async_asgi(receive, send):
if framework:
transaction.add_framework_info(name=framework[0], version=framework[1])

if dispatcher:
transaction.add_dispatcher_info(name=dispatcher[0], version=dispatcher[1])

# Override the initial web transaction name to be the supplied
# name, or the name of the wrapped callable if wanting to use
# the callable as the default. This will override the use of a
Expand Down
9 changes: 9 additions & 0 deletions newrelic/api/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ def __init__(self, application, enabled=None, source=None):

self._frameworks = set()
self._message_brokers = set()
self._dispatchers = set()

self._frozen_path = None

Expand Down Expand Up @@ -550,6 +551,10 @@ def __exit__(self, exc, value, tb):
for message_broker, version in self._message_brokers:
self.record_custom_metric("Python/MessageBroker/%s/%s" % (message_broker, version), 1)

if self._dispatchers:
for dispatcher, version in self._dispatchers:
self.record_custom_metric("Python/Dispatcher/%s/%s" % (dispatcher, version), 1)

if self._settings.distributed_tracing.enabled:
# Sampled and priority need to be computed at the end of the
# transaction when distributed tracing or span events are enabled.
Expand Down Expand Up @@ -1701,6 +1706,10 @@ def add_messagebroker_info(self, name, version=None):
if name:
self._message_brokers.add((name, version))

def add_dispatcher_info(self, name, version=None):
if name:
self._dispatchers.add((name, version))

def dump(self, file):
"""Dumps details about the transaction to the file object."""

Expand Down
19 changes: 11 additions & 8 deletions newrelic/api/wsgi_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
import time

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, FunctionTraceWrapper
from newrelic.api.html_insertion import insert_html_snippet, verify_body_exists
from newrelic.api.time_trace import notice_error
Expand Down Expand Up @@ -80,12 +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"):
FunctionTraceWrapper(self.generator.close)()

except: # Catch all
Expand Down Expand Up @@ -437,7 +434,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"):
FunctionTraceWrapper(self.iterable.close)()

def __iter__(self):
Expand Down Expand Up @@ -510,7 +507,7 @@ 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, dispatcher=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.
Expand Down Expand Up @@ -556,6 +553,9 @@ def _nr_wsgi_application_wrapper_(wrapped, instance, args, kwargs):
if framework:
transaction.add_framework_info(name=framework[0], version=framework[1])

if dispatcher:
transaction.add_dispatcher_info(name=dispatcher[0], version=dispatcher[1])

# Also override the web transaction name to be the name of
# the wrapped callable if not explicitly named, and we want
# the default name to be that of the WSGI component for the
Expand Down Expand Up @@ -618,6 +618,9 @@ def _args(environ, start_response, *args, **kwargs):
if framework:
transaction.add_framework_info(name=framework[0], version=framework[1])

if dispatcher:
transaction.add_dispatcher_info(name=dispatcher[0], version=dispatcher[1])

# Override the initial web transaction name to be the supplied
# name, or the name of the wrapped callable if wanting to use
# the callable as the default. This will override the use of a
Expand Down Expand Up @@ -672,7 +675,7 @@ def write(data):
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), source=wrapped):
if settings and settings.browser_monitoring.enabled and not transaction.autorum_disabled:
result = _WSGIApplicationMiddleware(wrapped, environ, _start_response, transaction)
Expand Down
4 changes: 3 additions & 1 deletion newrelic/hooks/adapter_daphne.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

from newrelic.api.asgi_application import ASGIApplicationWrapper
from newrelic.common.package_version_utils import get_package_version


@property
Expand All @@ -22,9 +23,10 @@ def application(self):

@application.setter
def application(self, value):
dispatcher_details = ("Daphne", get_package_version("daphne"))
# Wrap app only once
if value and not getattr(value, "_nr_wrapped", False):
value = ASGIApplicationWrapper(value)
value = ASGIApplicationWrapper(value, dispatcher=dispatcher_details)
value._nr_wrapped = True
self._nr_application = value

Expand Down
9 changes: 6 additions & 3 deletions newrelic/hooks/adapter_hypercorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from newrelic.api.asgi_application import ASGIApplicationWrapper
from newrelic.api.wsgi_application import WSGIApplicationWrapper
from newrelic.common.object_wrapper import wrap_function_wrapper
from newrelic.common.package_version_utils import get_package_version


def bind_worker_serve(app, *args, **kwargs):
Expand All @@ -24,6 +25,7 @@ def bind_worker_serve(app, *args, **kwargs):
async def wrap_worker_serve(wrapped, instance, args, kwargs):
import hypercorn

dispatcher_details = ("Hypercorn", get_package_version("hypercorn"))
wrapper_module = getattr(hypercorn, "app_wrappers", None)
asgi_wrapper_class = getattr(wrapper_module, "ASGIWrapper", None)
wsgi_wrapper_class = getattr(wrapper_module, "WSGIWrapper", None)
Expand All @@ -32,13 +34,14 @@ async def wrap_worker_serve(wrapped, instance, args, kwargs):

# Hypercorn 0.14.1 introduced wrappers for ASGI and WSGI apps that need to be above our instrumentation.
if asgi_wrapper_class is not None and isinstance(app, asgi_wrapper_class):
app.app = ASGIApplicationWrapper(app.app)
app.app = ASGIApplicationWrapper(app.app, dispatcher=dispatcher_details)
elif wsgi_wrapper_class is not None and isinstance(app, wsgi_wrapper_class):
app.app = WSGIApplicationWrapper(app.app)
app.app = WSGIApplicationWrapper(app.app, dispatcher=dispatcher_details)
else:
app = ASGIApplicationWrapper(app)
app = ASGIApplicationWrapper(app, dispatcher=dispatcher_details)

app._nr_wrapped = True

return await wrapped(app, *args, **kwargs)


Expand Down
7 changes: 6 additions & 1 deletion tests/adapter_daphne/test_daphne.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ async def fake_app(*args, **kwargs):

@override_application_settings({"transaction_name.naming_scheme": "framework"})
def test_daphne_200(port, app):
@validate_transaction_metrics(callable_name(app))
@validate_transaction_metrics(
callable_name(app),
custom_metrics=[
("Python/Dispatcher/Daphne/%s" % daphne.__version__, 1),
],
)
@raise_background_exceptions()
@wait_for_background_threads()
def response():
Expand Down
9 changes: 8 additions & 1 deletion tests/adapter_hypercorn/test_hypercorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,14 @@ def wait_for_port(port, retries=10):

@override_application_settings({"transaction_name.naming_scheme": "framework"})
def test_hypercorn_200(port, app):
@validate_transaction_metrics(callable_name(app))
hypercorn_version = pkg_resources.get_distribution("hypercorn").version

@validate_transaction_metrics(
callable_name(app),
custom_metrics=[
("Python/Dispatcher/Hypercorn/%s" % hypercorn_version, 1),
],
)
@raise_background_exceptions()
@wait_for_background_threads()
def response():
Expand Down

0 comments on commit 922db2f

Please sign in to comment.