Skip to content

Commit b58640c

Browse files
committed
Work on unit tests with AsyncClient
1 parent 864320a commit b58640c

File tree

4 files changed

+324
-12
lines changed

4 files changed

+324
-12
lines changed

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,10 @@ def process_request(self, request):
167167
),
168168
)
169169

170-
attributes = collect_request_attributes(request_meta)
170+
if is_asgi_request:
171+
attributes = collect_request_attributes(request.scope)
172+
else:
173+
attributes = collect_request_attributes(request_meta)
171174

172175
if span.is_recording():
173176
attributes = extract_attributes_from_object(

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from sys import modules
1616
from unittest.mock import Mock, patch
1717

18-
from django import VERSION
19-
from django.conf import settings
18+
from django import VERSION, conf
2019
from django.conf.urls import url
2120
from django.http import HttpRequest, HttpResponse
22-
from django.test import Client
21+
from django.test.client import Client
2322
from django.test.utils import setup_test_environment, teardown_test_environment
23+
from django.urls import re_path
2424

2525
from opentelemetry.instrumentation.django import (
2626
DjangoInstrumentor,
@@ -57,13 +57,13 @@
5757
DJANGO_2_2 = VERSION >= (2, 2)
5858

5959
urlpatterns = [
60-
url(r"^traced/", traced),
61-
url(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
62-
url(r"^error/", error),
63-
url(r"^excluded_arg/", excluded),
64-
url(r"^excluded_noarg/", excluded_noarg),
65-
url(r"^excluded_noarg2/", excluded_noarg2),
66-
url(r"^span_name/([0-9]{4})/$", route_span_name),
60+
re_path(r"^traced/", traced),
61+
re_path(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
62+
re_path(r"^error/", error),
63+
re_path(r"^excluded_arg/", excluded),
64+
re_path(r"^excluded_noarg/", excluded_noarg),
65+
re_path(r"^excluded_noarg2/", excluded_noarg2),
66+
re_path(r"^span_name/([0-9]{4})/$", route_span_name),
6767
]
6868
_django_instrumentor = DjangoInstrumentor()
6969

@@ -72,7 +72,7 @@ class TestMiddleware(TestBase, WsgiTestBase):
7272
@classmethod
7373
def setUpClass(cls):
7474
super().setUpClass()
75-
settings.configure(ROOT_URLCONF=modules[__name__])
75+
conf.settings.configure(ROOT_URLCONF=modules[__name__])
7676

7777
def setUp(self):
7878
super().setUp()
@@ -105,6 +105,11 @@ def tearDown(self):
105105
teardown_test_environment()
106106
_django_instrumentor.uninstrument()
107107

108+
@classmethod
109+
def tearDownClass(cls):
110+
super().tearDownClass()
111+
conf.settings = conf.LazySettings()
112+
108113
def test_templated_route_get(self):
109114
Client().get("/route/2020/template/")
110115

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
# Copyright The OpenTelemetry Authors
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 sys import modules
16+
from unittest.mock import Mock, patch
17+
18+
from django import VERSION, conf
19+
from django.test import SimpleTestCase
20+
from django.test.utils import setup_test_environment, teardown_test_environment
21+
from django.urls import re_path
22+
import pytest
23+
24+
from opentelemetry.instrumentation.django import DjangoInstrumentor
25+
from opentelemetry.test.test_base import TestBase
26+
from opentelemetry.trace import SpanKind, StatusCode
27+
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
28+
29+
# pylint: disable=import-error
30+
from .views import (
31+
async_error,
32+
async_excluded,
33+
async_excluded_noarg,
34+
async_excluded_noarg2,
35+
async_route_span_name,
36+
async_traced,
37+
async_traced_template,
38+
)
39+
40+
DJANGO_3_1 = VERSION >= (3, 1)
41+
42+
if DJANGO_3_1:
43+
from django.test.client import AsyncClient
44+
else:
45+
AsyncClient = None
46+
47+
urlpatterns = [
48+
re_path(r"^traced/", async_traced),
49+
re_path(r"^route/(?P<year>[0-9]{4})/template/$", async_traced_template),
50+
re_path(r"^error/", async_error),
51+
re_path(r"^excluded_arg/", async_excluded),
52+
re_path(r"^excluded_noarg/", async_excluded_noarg),
53+
re_path(r"^excluded_noarg2/", async_excluded_noarg2),
54+
re_path(r"^span_name/([0-9]{4})/$", async_route_span_name),
55+
]
56+
_django_instrumentor = DjangoInstrumentor()
57+
58+
59+
@pytest.mark.skipif(not DJANGO_3_1, reason="AsyncClient implemented since Django 3.1")
60+
class TestMiddlewareAsgi(SimpleTestCase, TestBase):
61+
62+
@classmethod
63+
def setUpClass(cls):
64+
super().setUpClass()
65+
conf.settings.configure(ROOT_URLCONF=modules[__name__])
66+
67+
def setUp(self):
68+
super().setUp()
69+
setup_test_environment()
70+
_django_instrumentor.instrument()
71+
self.env_patch = patch.dict(
72+
"os.environ",
73+
{
74+
"OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "http://testserver/excluded_arg/123,excluded_noarg",
75+
"OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS": "path_info,content_type,non_existing_variable",
76+
},
77+
)
78+
self.env_patch.start()
79+
self.exclude_patch = patch(
80+
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
81+
get_excluded_urls("DJANGO"),
82+
)
83+
self.traced_patch = patch(
84+
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs",
85+
get_traced_request_attrs("DJANGO"),
86+
)
87+
self.exclude_patch.start()
88+
self.traced_patch.start()
89+
90+
def tearDown(self):
91+
super().tearDown()
92+
self.env_patch.stop()
93+
self.exclude_patch.stop()
94+
self.traced_patch.stop()
95+
teardown_test_environment()
96+
_django_instrumentor.uninstrument()
97+
98+
@classmethod
99+
def tearDownClass(cls):
100+
super().tearDownClass()
101+
conf.settings = conf.LazySettings()
102+
103+
@classmethod
104+
def _add_databases_failures(cls):
105+
# Disable databases.
106+
pass
107+
108+
@classmethod
109+
def _remove_databases_failures(cls):
110+
# Disable databases.
111+
pass
112+
113+
@pytest.mark.skip(reason="TODO")
114+
async def test_templated_route_get(self):
115+
await self.async_client.get("/route/2020/template/")
116+
117+
spans = self.memory_exporter.get_finished_spans()
118+
self.assertEqual(len(spans), 1)
119+
120+
span = spans[0]
121+
122+
self.assertEqual(span.name, "^route/(?P<year>[0-9]{4})/template/$")
123+
self.assertEqual(span.kind, SpanKind.SERVER)
124+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
125+
self.assertEqual(span.attributes["http.method"], "GET")
126+
self.assertEqual(
127+
span.attributes["http.url"],
128+
"http://testserver/route/2020/template/",
129+
)
130+
self.assertEqual(
131+
span.attributes["http.route"],
132+
"^route/(?P<year>[0-9]{4})/template/$",
133+
)
134+
self.assertEqual(span.attributes["http.scheme"], "http")
135+
self.assertEqual(span.attributes["http.status_code"], 200)
136+
self.assertEqual(span.attributes["http.status_text"], "OK")
137+
138+
@pytest.mark.skip(reason="TODO")
139+
async def test_traced_get(self):
140+
await self.async_client.get("/traced/")
141+
142+
spans = self.memory_exporter.get_finished_spans()
143+
self.assertEqual(len(spans), 1)
144+
145+
span = spans[0]
146+
147+
self.assertEqual(span.name, "^traced/")
148+
self.assertEqual(span.kind, SpanKind.SERVER)
149+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
150+
self.assertEqual(span.attributes["http.method"], "GET")
151+
self.assertEqual(
152+
span.attributes["http.url"], "http://testserver/traced/"
153+
)
154+
self.assertEqual(span.attributes["http.route"], "^traced/")
155+
self.assertEqual(span.attributes["http.scheme"], "http")
156+
self.assertEqual(span.attributes["http.status_code"], 200)
157+
self.assertEqual(span.attributes["http.status_text"], "OK")
158+
159+
async def test_not_recording(self):
160+
mock_tracer = Mock()
161+
mock_span = Mock()
162+
mock_span.is_recording.return_value = False
163+
mock_tracer.start_span.return_value = mock_span
164+
with patch("opentelemetry.trace.get_tracer") as tracer:
165+
tracer.return_value = mock_tracer
166+
await self.async_client.get("/traced/")
167+
self.assertFalse(mock_span.is_recording())
168+
self.assertTrue(mock_span.is_recording.called)
169+
self.assertFalse(mock_span.set_attribute.called)
170+
self.assertFalse(mock_span.set_status.called)
171+
172+
@pytest.mark.skip(reason="TODO")
173+
async def test_traced_post(self):
174+
await self.async_client.post("/traced/")
175+
176+
spans = self.memory_exporter.get_finished_spans()
177+
self.assertEqual(len(spans), 1)
178+
179+
span = spans[0]
180+
181+
self.assertEqual(span.name, "^traced/")
182+
self.assertEqual(span.kind, SpanKind.SERVER)
183+
self.assertEqual(span.status.status_code, StatusCode.UNSET)
184+
self.assertEqual(span.attributes["http.method"], "POST")
185+
self.assertEqual(
186+
span.attributes["http.url"], "http://testserver/traced/"
187+
)
188+
self.assertEqual(span.attributes["http.route"], "^traced/")
189+
self.assertEqual(span.attributes["http.scheme"], "http")
190+
self.assertEqual(span.attributes["http.status_code"], 200)
191+
self.assertEqual(span.attributes["http.status_text"], "OK")
192+
193+
@pytest.mark.skip(reason="TODO")
194+
async def test_error(self):
195+
with self.assertRaises(ValueError):
196+
await self.async_client.get("/error/")
197+
198+
spans = self.memory_exporter.get_finished_spans()
199+
self.assertEqual(len(spans), 1)
200+
201+
span = spans[0]
202+
203+
self.assertEqual(span.name, "^error/")
204+
self.assertEqual(span.kind, SpanKind.SERVER)
205+
self.assertEqual(span.status.status_code, StatusCode.ERROR)
206+
self.assertEqual(span.attributes["http.method"], "GET")
207+
self.assertEqual(
208+
span.attributes["http.url"], "http://testserver/error/"
209+
)
210+
self.assertEqual(span.attributes["http.route"], "^error/")
211+
self.assertEqual(span.attributes["http.scheme"], "http")
212+
self.assertEqual(span.attributes["http.status_code"], 500)
213+
214+
self.assertEqual(len(span.events), 1)
215+
event = span.events[0]
216+
self.assertEqual(event.name, "exception")
217+
self.assertEqual(event.attributes["exception.type"], "ValueError")
218+
self.assertEqual(event.attributes["exception.message"], "error")
219+
220+
async def test_exclude_lists(self):
221+
await self.async_client.get("/excluded_arg/123")
222+
span_list = self.memory_exporter.get_finished_spans()
223+
self.assertEqual(len(span_list), 0)
224+
225+
await self.async_client.get("/excluded_arg/125")
226+
span_list = self.memory_exporter.get_finished_spans()
227+
self.assertEqual(len(span_list), 1)
228+
229+
await self.async_client.get("/excluded_noarg/")
230+
span_list = self.memory_exporter.get_finished_spans()
231+
self.assertEqual(len(span_list), 1)
232+
233+
await self.async_client.get("/excluded_noarg2/")
234+
span_list = self.memory_exporter.get_finished_spans()
235+
self.assertEqual(len(span_list), 1)
236+
237+
async def test_span_name(self):
238+
# test no query_string
239+
await self.async_client.get("/span_name/1234/")
240+
span_list = self.memory_exporter.get_finished_spans()
241+
self.assertEqual(len(span_list), 1)
242+
243+
span = span_list[0]
244+
self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
245+
246+
async def test_span_name_for_query_string(self):
247+
"""
248+
request not have query string
249+
"""
250+
await self.async_client.get("/span_name/1234/?query=test")
251+
span_list = self.memory_exporter.get_finished_spans()
252+
self.assertEqual(len(span_list), 1)
253+
254+
span = span_list[0]
255+
self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
256+
257+
async def test_span_name_404(self):
258+
await self.async_client.get("/span_name/1234567890/")
259+
span_list = self.memory_exporter.get_finished_spans()
260+
self.assertEqual(len(span_list), 1)
261+
262+
span = span_list[0]
263+
self.assertEqual(span.name, "HTTP GET")
264+
265+
@pytest.mark.skip(reason="TODO")
266+
async def test_traced_request_attrs(self):
267+
await self.async_client.get("/span_name/1234/", CONTENT_TYPE="test/ct")
268+
span_list = self.memory_exporter.get_finished_spans()
269+
self.assertEqual(len(span_list), 1)
270+
271+
span = span_list[0]
272+
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
273+
self.assertEqual(span.attributes["content_type"], "test/ct")
274+
self.assertNotIn("non_existing_variable", span.attributes)

instrumentation/opentelemetry-instrumentation-django/tests/views.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,33 @@ def route_span_name(
2929
request, *args, **kwargs
3030
): # pylint: disable=unused-argument
3131
return HttpResponse()
32+
33+
34+
async def async_traced(request): # pylint: disable=unused-argument
35+
return HttpResponse()
36+
37+
38+
async def async_traced_template(request, year): # pylint: disable=unused-argument
39+
return HttpResponse()
40+
41+
42+
async def async_error(request): # pylint: disable=unused-argument
43+
raise ValueError("error")
44+
45+
46+
async def async_excluded(request): # pylint: disable=unused-argument
47+
return HttpResponse()
48+
49+
50+
async def async_excluded_noarg(request): # pylint: disable=unused-argument
51+
return HttpResponse()
52+
53+
54+
async def async_excluded_noarg2(request): # pylint: disable=unused-argument
55+
return HttpResponse()
56+
57+
58+
async def async_route_span_name(
59+
request, *args, **kwargs
60+
): # pylint: disable=unused-argument
61+
return HttpResponse()

0 commit comments

Comments
 (0)