Skip to content

Commit 2633a4d

Browse files
authored
Daphne ASGI Server Instrumentation (#597)
* Daphne instrumentation * Daphne Testing * Add Daphne ASGI v2 testing * Fix flake8 errors * Apply linter fixes * Remove py36 testing
1 parent e891528 commit 2633a4d

File tree

7 files changed

+225
-2
lines changed

7 files changed

+225
-2
lines changed

newrelic/config.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -2540,6 +2540,8 @@ def _process_module_builtin_defaults():
25402540

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

2543+
_process_module_definition("daphne.server", "newrelic.hooks.adapter_daphne", "instrument_daphne_server")
2544+
25432545
_process_module_definition("sanic.app", "newrelic.hooks.framework_sanic", "instrument_sanic_app")
25442546
_process_module_definition("sanic.response", "newrelic.hooks.framework_sanic", "instrument_sanic_response")
25452547

@@ -2712,7 +2714,9 @@ def _process_module_builtin_defaults():
27122714
)
27132715

27142716
_process_module_definition(
2715-
"redis.commands.timeseries.commands", "newrelic.hooks.datastore_redis", "instrument_redis_commands_timeseries_commands"
2717+
"redis.commands.timeseries.commands",
2718+
"newrelic.hooks.datastore_redis",
2719+
"instrument_redis_commands_timeseries_commands",
27162720
)
27172721

27182722
_process_module_definition(

newrelic/core/environment.py

+7
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,13 @@ def environment_settings():
170170
if hasattr(uvicorn, "__version__"):
171171
dispatcher.append(("Dispatcher Version", uvicorn.__version__))
172172

173+
if not dispatcher and "daphne" in sys.modules:
174+
dispatcher.append(("Dispatcher", "daphne"))
175+
daphne = sys.modules["daphne"]
176+
177+
if hasattr(daphne, "__version__"):
178+
dispatcher.append(("Dispatcher Version", daphne.__version__))
179+
173180
if not dispatcher and "tornado" in sys.modules:
174181
dispatcher.append(("Dispatcher", "tornado"))
175182
tornado = sys.modules["tornado"]

newrelic/hooks/adapter_daphne.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from newrelic.api.asgi_application import ASGIApplicationWrapper
16+
17+
18+
@property
19+
def application(self):
20+
return getattr(self, "_nr_application", vars(self).get("application", None))
21+
22+
23+
@application.setter
24+
def application(self, value):
25+
# Wrap app only once
26+
if value and not getattr(value, "_nr_wrapped", False):
27+
value = ASGIApplicationWrapper(value)
28+
value._nr_wrapped = True
29+
self._nr_application = value
30+
31+
32+
def instrument_daphne_server(module):
33+
module.Server.application = application

tests/adapter_daphne/conftest.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from testing_support.fixtures import ( # noqa: F401; pylint: disable=W0611
16+
code_coverage_fixture,
17+
collector_agent_registration_fixture,
18+
collector_available_fixture,
19+
)
20+
21+
_coverage_source = [
22+
"newrelic.hooks.adapter_daphne",
23+
]
24+
25+
code_coverage = code_coverage_fixture(source=_coverage_source)
26+
27+
_default_settings = {
28+
"transaction_tracer.explain_threshold": 0.0,
29+
"transaction_tracer.transaction_threshold": 0.0,
30+
"transaction_tracer.stack_trace_threshold": 0.0,
31+
"debug.log_data_collector_payloads": True,
32+
"debug.record_transaction_failure": True,
33+
}
34+
35+
collector_agent_registration = collector_agent_registration_fixture(
36+
app_name="Python Agent Test (adapter_daphne)", default_settings=_default_settings
37+
)

tests/adapter_daphne/test_daphne.py

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import asyncio
16+
import threading
17+
from urllib.request import HTTPError, urlopen
18+
19+
import daphne.server
20+
import pytest
21+
from testing_support.fixtures import (
22+
override_application_settings,
23+
raise_background_exceptions,
24+
validate_transaction_errors,
25+
validate_transaction_metrics,
26+
wait_for_background_threads,
27+
)
28+
from testing_support.sample_asgi_applications import (
29+
AppWithCall,
30+
AppWithCallRaw,
31+
simple_app_v2_raw,
32+
)
33+
from testing_support.util import get_open_port
34+
35+
from newrelic.common.object_names import callable_name
36+
37+
DAPHNE_VERSION = tuple(int(v) for v in daphne.__version__.split(".")[:2])
38+
skip_asgi_3_unsupported = pytest.mark.skipif(DAPHNE_VERSION < (3, 0), reason="ASGI3 unsupported")
39+
skip_asgi_2_unsupported = pytest.mark.skipif(DAPHNE_VERSION >= (3, 0), reason="ASGI2 unsupported")
40+
41+
42+
@pytest.fixture(
43+
params=(
44+
pytest.param(
45+
simple_app_v2_raw,
46+
marks=skip_asgi_2_unsupported,
47+
),
48+
pytest.param(
49+
AppWithCallRaw(),
50+
marks=skip_asgi_3_unsupported,
51+
),
52+
pytest.param(
53+
AppWithCall(),
54+
marks=skip_asgi_3_unsupported,
55+
),
56+
),
57+
ids=("raw", "class_with_call", "class_with_call_double_wrapped"),
58+
)
59+
def app(request, server_and_port):
60+
app = request.param
61+
server, _ = server_and_port
62+
server.application = app
63+
return app
64+
65+
66+
@pytest.fixture(scope="session")
67+
def port(server_and_port):
68+
_, port = server_and_port
69+
return port
70+
71+
72+
@pytest.fixture(scope="session")
73+
def server_and_port():
74+
port = get_open_port()
75+
76+
servers = []
77+
loops = []
78+
ready = threading.Event()
79+
80+
def server_run():
81+
def on_ready():
82+
if not ready.is_set():
83+
loops.append(asyncio.get_event_loop())
84+
servers.append(server)
85+
ready.set()
86+
87+
async def fake_app(*args, **kwargs):
88+
raise RuntimeError("Failed to swap out app.")
89+
90+
server = daphne.server.Server(
91+
fake_app,
92+
endpoints=["tcp:%d:interface=127.0.0.1" % port],
93+
ready_callable=on_ready,
94+
signal_handlers=False,
95+
verbosity=9,
96+
)
97+
98+
server.run()
99+
100+
thread = threading.Thread(target=server_run, daemon=True)
101+
thread.start()
102+
assert ready.wait(timeout=10)
103+
yield servers[0], port
104+
105+
reactor = daphne.server.reactor
106+
_ = [loop.call_soon_threadsafe(reactor.stop) for loop in loops] # Stop all loops
107+
thread.join(timeout=10)
108+
109+
if thread.is_alive():
110+
raise RuntimeError("Thread failed to exit in time.")
111+
112+
113+
@override_application_settings({"transaction_name.naming_scheme": "framework"})
114+
def test_daphne_200(port, app):
115+
@validate_transaction_metrics(callable_name(app))
116+
@raise_background_exceptions()
117+
@wait_for_background_threads()
118+
def response():
119+
return urlopen("http://localhost:%d" % port, timeout=10)
120+
121+
assert response().status == 200
122+
123+
124+
@override_application_settings({"transaction_name.naming_scheme": "framework"})
125+
@validate_transaction_errors(["builtins:ValueError"])
126+
def test_daphne_500(port, app):
127+
@validate_transaction_metrics(callable_name(app))
128+
@raise_background_exceptions()
129+
@wait_for_background_threads()
130+
def _test():
131+
try:
132+
urlopen("http://localhost:%d/exc" % port)
133+
except HTTPError:
134+
pass
135+
136+
_test()

tests/adapter_uvicorn/test_uvicorn.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async def on_tick():
9797

9898
thread = threading.Thread(target=server_run, daemon=True)
9999
thread.start()
100-
ready.wait()
100+
assert ready.wait(timeout=10)
101101
yield port
102102
_ = [loop.stop() for loop in loops] # Stop all loops
103103
thread.join(timeout=1)

tox.ini

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
setupdir = {toxinidir}
4444
envlist =
4545
python-adapter_cheroot-{py27,py37,py38,py39,py310},
46+
python-adapter_daphne-{py37,py38,py39,py310}-daphnelatest,
47+
python-adapter_daphne-py38-daphne{0204,0205},
4648
python-adapter_gevent-{py27,py37,py38,py310},
4749
python-adapter_gunicorn-{py37,py38,py39,py310}-aiohttp3-gunicornlatest,
4850
python-adapter_uvicorn-py37-uvicorn03,
@@ -163,6 +165,9 @@ deps =
163165

164166
# Test Suite Dependencies
165167
adapter_cheroot: cheroot
168+
adapter_daphne-daphnelatest: daphne
169+
adapter_daphne-daphne0205: daphne<2.6
170+
adapter_daphne-daphne0204: daphne<2.5
166171
adapter_gevent: WSGIProxy2
167172
adapter_gevent: gevent
168173
adapter_gevent: urllib3
@@ -372,6 +377,7 @@ extras =
372377

373378
changedir =
374379
adapter_cheroot: tests/adapter_cheroot
380+
adapter_daphne: tests/adapter_daphne
375381
adapter_gevent: tests/adapter_gevent
376382
adapter_gunicorn: tests/adapter_gunicorn
377383
adapter_uvicorn: tests/adapter_uvicorn

0 commit comments

Comments
 (0)