Skip to content

Commit

Permalink
Daphne ASGI Server Instrumentation (#597)
Browse files Browse the repository at this point in the history
* Daphne instrumentation

* Daphne Testing

* Add Daphne ASGI v2 testing

* Fix flake8 errors

* Apply linter fixes

* Remove py36 testing
  • Loading branch information
TimPansino committed Aug 15, 2022
1 parent e891528 commit 2633a4d
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 2 deletions.
6 changes: 5 additions & 1 deletion newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2540,6 +2540,8 @@ def _process_module_builtin_defaults():

_process_module_definition("uvicorn.config", "newrelic.hooks.adapter_uvicorn", "instrument_uvicorn_config")

_process_module_definition("daphne.server", "newrelic.hooks.adapter_daphne", "instrument_daphne_server")

_process_module_definition("sanic.app", "newrelic.hooks.framework_sanic", "instrument_sanic_app")
_process_module_definition("sanic.response", "newrelic.hooks.framework_sanic", "instrument_sanic_response")

Expand Down Expand Up @@ -2712,7 +2714,9 @@ def _process_module_builtin_defaults():
)

_process_module_definition(
"redis.commands.timeseries.commands", "newrelic.hooks.datastore_redis", "instrument_redis_commands_timeseries_commands"
"redis.commands.timeseries.commands",
"newrelic.hooks.datastore_redis",
"instrument_redis_commands_timeseries_commands",
)

_process_module_definition(
Expand Down
7 changes: 7 additions & 0 deletions newrelic/core/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ def environment_settings():
if hasattr(uvicorn, "__version__"):
dispatcher.append(("Dispatcher Version", uvicorn.__version__))

if not dispatcher and "daphne" in sys.modules:
dispatcher.append(("Dispatcher", "daphne"))
daphne = sys.modules["daphne"]

if hasattr(daphne, "__version__"):
dispatcher.append(("Dispatcher Version", daphne.__version__))

if not dispatcher and "tornado" in sys.modules:
dispatcher.append(("Dispatcher", "tornado"))
tornado = sys.modules["tornado"]
Expand Down
33 changes: 33 additions & 0 deletions newrelic/hooks/adapter_daphne.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from newrelic.api.asgi_application import ASGIApplicationWrapper


@property
def application(self):
return getattr(self, "_nr_application", vars(self).get("application", None))


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


def instrument_daphne_server(module):
module.Server.application = application
37 changes: 37 additions & 0 deletions tests/adapter_daphne/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from testing_support.fixtures import ( # noqa: F401; pylint: disable=W0611
code_coverage_fixture,
collector_agent_registration_fixture,
collector_available_fixture,
)

_coverage_source = [
"newrelic.hooks.adapter_daphne",
]

code_coverage = code_coverage_fixture(source=_coverage_source)

_default_settings = {
"transaction_tracer.explain_threshold": 0.0,
"transaction_tracer.transaction_threshold": 0.0,
"transaction_tracer.stack_trace_threshold": 0.0,
"debug.log_data_collector_payloads": True,
"debug.record_transaction_failure": True,
}

collector_agent_registration = collector_agent_registration_fixture(
app_name="Python Agent Test (adapter_daphne)", default_settings=_default_settings
)
136 changes: 136 additions & 0 deletions tests/adapter_daphne/test_daphne.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
import threading
from urllib.request import HTTPError, urlopen

import daphne.server
import pytest
from testing_support.fixtures import (
override_application_settings,
raise_background_exceptions,
validate_transaction_errors,
validate_transaction_metrics,
wait_for_background_threads,
)
from testing_support.sample_asgi_applications import (
AppWithCall,
AppWithCallRaw,
simple_app_v2_raw,
)
from testing_support.util import get_open_port

from newrelic.common.object_names import callable_name

DAPHNE_VERSION = tuple(int(v) for v in daphne.__version__.split(".")[:2])
skip_asgi_3_unsupported = pytest.mark.skipif(DAPHNE_VERSION < (3, 0), reason="ASGI3 unsupported")
skip_asgi_2_unsupported = pytest.mark.skipif(DAPHNE_VERSION >= (3, 0), reason="ASGI2 unsupported")


@pytest.fixture(
params=(
pytest.param(
simple_app_v2_raw,
marks=skip_asgi_2_unsupported,
),
pytest.param(
AppWithCallRaw(),
marks=skip_asgi_3_unsupported,
),
pytest.param(
AppWithCall(),
marks=skip_asgi_3_unsupported,
),
),
ids=("raw", "class_with_call", "class_with_call_double_wrapped"),
)
def app(request, server_and_port):
app = request.param
server, _ = server_and_port
server.application = app
return app


@pytest.fixture(scope="session")
def port(server_and_port):
_, port = server_and_port
return port


@pytest.fixture(scope="session")
def server_and_port():
port = get_open_port()

servers = []
loops = []
ready = threading.Event()

def server_run():
def on_ready():
if not ready.is_set():
loops.append(asyncio.get_event_loop())
servers.append(server)
ready.set()

async def fake_app(*args, **kwargs):
raise RuntimeError("Failed to swap out app.")

server = daphne.server.Server(
fake_app,
endpoints=["tcp:%d:interface=127.0.0.1" % port],
ready_callable=on_ready,
signal_handlers=False,
verbosity=9,
)

server.run()

thread = threading.Thread(target=server_run, daemon=True)
thread.start()
assert ready.wait(timeout=10)
yield servers[0], port

reactor = daphne.server.reactor
_ = [loop.call_soon_threadsafe(reactor.stop) for loop in loops] # Stop all loops
thread.join(timeout=10)

if thread.is_alive():
raise RuntimeError("Thread failed to exit in time.")


@override_application_settings({"transaction_name.naming_scheme": "framework"})
def test_daphne_200(port, app):
@validate_transaction_metrics(callable_name(app))
@raise_background_exceptions()
@wait_for_background_threads()
def response():
return urlopen("http://localhost:%d" % port, timeout=10)

assert response().status == 200


@override_application_settings({"transaction_name.naming_scheme": "framework"})
@validate_transaction_errors(["builtins:ValueError"])
def test_daphne_500(port, app):
@validate_transaction_metrics(callable_name(app))
@raise_background_exceptions()
@wait_for_background_threads()
def _test():
try:
urlopen("http://localhost:%d/exc" % port)
except HTTPError:
pass

_test()
2 changes: 1 addition & 1 deletion tests/adapter_uvicorn/test_uvicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async def on_tick():

thread = threading.Thread(target=server_run, daemon=True)
thread.start()
ready.wait()
assert ready.wait(timeout=10)
yield port
_ = [loop.stop() for loop in loops] # Stop all loops
thread.join(timeout=1)
Expand Down
6 changes: 6 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
setupdir = {toxinidir}
envlist =
python-adapter_cheroot-{py27,py37,py38,py39,py310},
python-adapter_daphne-{py37,py38,py39,py310}-daphnelatest,
python-adapter_daphne-py38-daphne{0204,0205},
python-adapter_gevent-{py27,py37,py38,py310},
python-adapter_gunicorn-{py37,py38,py39,py310}-aiohttp3-gunicornlatest,
python-adapter_uvicorn-py37-uvicorn03,
Expand Down Expand Up @@ -163,6 +165,9 @@ deps =

# Test Suite Dependencies
adapter_cheroot: cheroot
adapter_daphne-daphnelatest: daphne
adapter_daphne-daphne0205: daphne<2.6
adapter_daphne-daphne0204: daphne<2.5
adapter_gevent: WSGIProxy2
adapter_gevent: gevent
adapter_gevent: urllib3
Expand Down Expand Up @@ -372,6 +377,7 @@ extras =

changedir =
adapter_cheroot: tests/adapter_cheroot
adapter_daphne: tests/adapter_daphne
adapter_gevent: tests/adapter_gevent
adapter_gunicorn: tests/adapter_gunicorn
adapter_uvicorn: tests/adapter_uvicorn
Expand Down

0 comments on commit 2633a4d

Please sign in to comment.