Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1750103

Browse files
committedMay 30, 2021
Add support for traced attributes
Add support for the `extract_attributes_from_object` function to retrieve traced attributes that are added to `request.scope.headers`. This is added based on the code found in [Django's `AsyncRequestFactory`]( https://github.com/django/django/blob/a948d9df394aafded78d72b1daa785a0abfeab48/django/test/client.py#L550-L553), which indicates that any extra argument sent to the `AsyncClient` is set in the `request.scope.headers` list. Also: * Stop inheriting from `SimpleTestCase` in async Django tests, to simplify test inheritance. * Correctly configure Django settings in tests, before calling parent's `setUpClass` method. * Rebase and port the latest changes to Django WSGI tests, to be supported by ASGI ones.
1 parent d51c8a9 commit 1750103

File tree

3 files changed

+172
-48
lines changed

3 files changed

+172
-48
lines changed
 

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

+21-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from logging import getLogger
1616
from time import time
17+
import types
1718
from typing import Callable
1819

1920
from django.http import HttpRequest, HttpResponse
@@ -174,9 +175,26 @@ def process_request(self, request):
174175
attributes = collect_request_attributes(request_meta)
175176

176177
if span.is_recording():
177-
attributes = extract_attributes_from_object(
178-
request, self._traced_request_attrs, attributes
179-
)
178+
if is_asgi_request:
179+
# ASGI requests include extra attributes in request.scope.headers. For this reason,
180+
# we need to build an object with the union of `request` and `request.scope.headers`
181+
# contents, for the extract_attributes_from_object function to be able to retrieve
182+
# attributes from it.
183+
attributes = extract_attributes_from_object(
184+
types.SimpleNamespace(**{
185+
**request.__dict__,
186+
**{
187+
name.decode('latin1'): value.decode('latin1')
188+
for name, value in request.scope.get('headers', [])
189+
},
190+
}),
191+
self._traced_request_attrs,
192+
attributes,
193+
)
194+
else:
195+
attributes = extract_attributes_from_object(
196+
request, self._traced_request_attrs, attributes
197+
)
180198
for key, value in attributes.items():
181199
span.set_attribute(key, value)
182200

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@
7171
class TestMiddleware(TestBase, WsgiTestBase):
7272
@classmethod
7373
def setUpClass(cls):
74-
super().setUpClass()
7574
conf.settings.configure(ROOT_URLCONF=modules[__name__])
75+
super().setUpClass()
7676

7777
def setUp(self):
7878
super().setUp()

‎instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py

+150-44
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,31 @@
1616
from unittest.mock import Mock, patch
1717

1818
from django import VERSION, conf
19+
from django.conf.urls import url
20+
from django.http import HttpRequest, HttpResponse
1921
from django.test import SimpleTestCase
2022
from django.test.utils import setup_test_environment, teardown_test_environment
2123
from django.urls import re_path
2224
import pytest
2325

24-
from opentelemetry.instrumentation.django import DjangoInstrumentor
26+
from opentelemetry.instrumentation.django import (
27+
DjangoInstrumentor,
28+
_DjangoMiddleware,
29+
)
30+
from opentelemetry.instrumentation.propagators import (
31+
TraceResponsePropagator,
32+
set_global_response_propagator,
33+
)
34+
from opentelemetry.sdk import resources
35+
from opentelemetry.sdk.trace import Span
36+
from opentelemetry.semconv.trace import SpanAttributes
2537
from opentelemetry.test.test_base import TestBase
26-
from opentelemetry.trace import SpanKind, StatusCode
38+
from opentelemetry.trace import (
39+
SpanKind,
40+
StatusCode,
41+
format_span_id,
42+
format_trace_id,
43+
)
2744
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
2845

2946
# pylint: disable=import-error
@@ -62,8 +79,8 @@
6279
class TestMiddlewareAsgi(SimpleTestCase, TestBase):
6380
@classmethod
6481
def setUpClass(cls):
65-
super().setUpClass()
6682
conf.settings.configure(ROOT_URLCONF=modules[__name__])
83+
super().setUpClass()
6784

6885
def setUp(self):
6986
super().setUp()
@@ -101,16 +118,6 @@ def tearDownClass(cls):
101118
super().tearDownClass()
102119
conf.settings = conf.LazySettings()
103120

104-
@classmethod
105-
def _add_databases_failures(cls):
106-
# Disable databases.
107-
pass
108-
109-
@classmethod
110-
def _remove_databases_failures(cls):
111-
# Disable databases.
112-
pass
113-
114121
async def test_templated_route_get(self):
115122
await self.async_client.get("/route/2020/template/")
116123

@@ -122,19 +129,17 @@ async def test_templated_route_get(self):
122129
self.assertEqual(span.name, "^route/(?P<year>[0-9]{4})/template/$")
123130
self.assertEqual(span.kind, SpanKind.SERVER)
124131
self.assertEqual(span.status.status_code, StatusCode.UNSET)
125-
self.assertEqual(span.attributes["http.method"], "GET")
132+
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
126133
self.assertEqual(
127-
span.attributes["http.url"],
134+
span.attributes[SpanAttributes.HTTP_URL],
128135
"http://127.0.0.1/route/2020/template/",
129136
)
130137
self.assertEqual(
131-
span.attributes["http.route"],
138+
span.attributes[SpanAttributes.HTTP_ROUTE],
132139
"^route/(?P<year>[0-9]{4})/template/$",
133140
)
134-
self.assertEqual(span.attributes["http.scheme"], "http")
135-
self.assertEqual(span.attributes["http.status_code"], 200)
136-
# TODO: Add http.status_text to ASGI instrumentation.
137-
# self.assertEqual(span.attributes["http.status_text"], "OK")
141+
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
142+
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
138143

139144
async def test_traced_get(self):
140145
await self.async_client.get("/traced/")
@@ -147,15 +152,16 @@ async def test_traced_get(self):
147152
self.assertEqual(span.name, "^traced/")
148153
self.assertEqual(span.kind, SpanKind.SERVER)
149154
self.assertEqual(span.status.status_code, StatusCode.UNSET)
150-
self.assertEqual(span.attributes["http.method"], "GET")
155+
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
156+
self.assertEqual(
157+
span.attributes[SpanAttributes.HTTP_URL],
158+
"http://127.0.0.1/traced/",
159+
)
151160
self.assertEqual(
152-
span.attributes["http.url"], "http://127.0.0.1/traced/"
161+
span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/"
153162
)
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-
# TODO: Add http.status_text to ASGI instrumentation.
158-
# self.assertEqual(span.attributes["http.status_text"], "OK")
163+
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
164+
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
159165

160166
async def test_not_recording(self):
161167
mock_tracer = Mock()
@@ -181,15 +187,16 @@ async def test_traced_post(self):
181187
self.assertEqual(span.name, "^traced/")
182188
self.assertEqual(span.kind, SpanKind.SERVER)
183189
self.assertEqual(span.status.status_code, StatusCode.UNSET)
184-
self.assertEqual(span.attributes["http.method"], "POST")
190+
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "POST")
191+
self.assertEqual(
192+
span.attributes[SpanAttributes.HTTP_URL],
193+
"http://127.0.0.1/traced/",
194+
)
185195
self.assertEqual(
186-
span.attributes["http.url"], "http://127.0.0.1/traced/"
196+
span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/"
187197
)
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-
# TODO: Add http.status_text to ASGI instrumentation.
192-
# self.assertEqual(span.attributes["http.status_text"], "OK")
198+
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
199+
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
193200

194201
async def test_error(self):
195202
with self.assertRaises(ValueError):
@@ -203,19 +210,24 @@ async def test_error(self):
203210
self.assertEqual(span.name, "^error/")
204211
self.assertEqual(span.kind, SpanKind.SERVER)
205212
self.assertEqual(span.status.status_code, StatusCode.ERROR)
206-
self.assertEqual(span.attributes["http.method"], "GET")
213+
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
207214
self.assertEqual(
208-
span.attributes["http.url"], "http://127.0.0.1/error/"
215+
span.attributes[SpanAttributes.HTTP_URL],
216+
"http://127.0.0.1/error/",
209217
)
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)
218+
self.assertEqual(span.attributes[SpanAttributes.HTTP_ROUTE], "^error/")
219+
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
220+
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 500)
213221

214222
self.assertEqual(len(span.events), 1)
215223
event = span.events[0]
216224
self.assertEqual(event.name, "exception")
217-
self.assertEqual(event.attributes["exception.type"], "ValueError")
218-
self.assertEqual(event.attributes["exception.message"], "error")
225+
self.assertEqual(
226+
event.attributes[SpanAttributes.EXCEPTION_TYPE], "ValueError"
227+
)
228+
self.assertEqual(
229+
event.attributes[SpanAttributes.EXCEPTION_MESSAGE], "error"
230+
)
219231

220232
async def test_exclude_lists(self):
221233
await self.async_client.get("/excluded_arg/123")
@@ -262,9 +274,6 @@ async def test_span_name_404(self):
262274
span = span_list[0]
263275
self.assertEqual(span.name, "HTTP GET")
264276

265-
@pytest.mark.skip(
266-
reason="TODO: Traced request attributes not supported yet"
267-
)
268277
async def test_traced_request_attrs(self):
269278
await self.async_client.get("/span_name/1234/", CONTENT_TYPE="test/ct")
270279
span_list = self.memory_exporter.get_finished_spans()
@@ -274,3 +283,100 @@ async def test_traced_request_attrs(self):
274283
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
275284
self.assertEqual(span.attributes["content_type"], "test/ct")
276285
self.assertNotIn("non_existing_variable", span.attributes)
286+
287+
async def test_hooks(self):
288+
request_hook_args = ()
289+
response_hook_args = ()
290+
291+
def request_hook(span, request):
292+
nonlocal request_hook_args
293+
request_hook_args = (span, request)
294+
295+
def response_hook(span, request, response):
296+
nonlocal response_hook_args
297+
response_hook_args = (span, request, response)
298+
response["hook-header"] = "set by hook"
299+
300+
_DjangoMiddleware._otel_request_hook = request_hook
301+
_DjangoMiddleware._otel_response_hook = response_hook
302+
303+
response = await self.async_client.get("/span_name/1234/")
304+
_DjangoMiddleware._otel_request_hook = (
305+
_DjangoMiddleware._otel_response_hook
306+
) = None
307+
308+
self.assertEqual(response["hook-header"], "set by hook")
309+
310+
span_list = self.memory_exporter.get_finished_spans()
311+
self.assertEqual(len(span_list), 1)
312+
span = span_list[0]
313+
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
314+
315+
self.assertEqual(len(request_hook_args), 2)
316+
self.assertEqual(request_hook_args[0].name, span.name)
317+
self.assertIsInstance(request_hook_args[0], Span)
318+
self.assertIsInstance(request_hook_args[1], HttpRequest)
319+
320+
self.assertEqual(len(response_hook_args), 3)
321+
self.assertEqual(request_hook_args[0], response_hook_args[0])
322+
self.assertIsInstance(response_hook_args[1], HttpRequest)
323+
self.assertIsInstance(response_hook_args[2], HttpResponse)
324+
self.assertEqual(response_hook_args[2], response)
325+
326+
async def test_trace_response_headers(self):
327+
response = await self.async_client.get("/span_name/1234/")
328+
329+
self.assertNotIn("Server-Timing", response.headers)
330+
self.memory_exporter.clear()
331+
332+
set_global_response_propagator(TraceResponsePropagator())
333+
334+
response = await self.async_client.get("/span_name/1234/")
335+
span = self.memory_exporter.get_finished_spans()[0]
336+
337+
self.assertIn("traceresponse", response.headers)
338+
self.assertEqual(
339+
response.headers["Access-Control-Expose-Headers"], "traceresponse",
340+
)
341+
self.assertEqual(
342+
response.headers["traceresponse"],
343+
"00-{0}-{1}-01".format(
344+
format_trace_id(span.get_span_context().trace_id),
345+
format_span_id(span.get_span_context().span_id),
346+
),
347+
)
348+
self.memory_exporter.clear()
349+
350+
351+
class TestMiddlewareAsgiWithTracerProvider(SimpleTestCase, TestBase):
352+
@classmethod
353+
def setUpClass(cls):
354+
super().setUpClass()
355+
356+
def setUp(self):
357+
super().setUp()
358+
setup_test_environment()
359+
resource = resources.Resource.create(
360+
{"resource-key": "resource-value"}
361+
)
362+
result = self.create_tracer_provider(resource=resource)
363+
tracer_provider, exporter = result
364+
self.exporter = exporter
365+
_django_instrumentor.instrument(tracer_provider=tracer_provider)
366+
367+
def tearDown(self):
368+
super().tearDown()
369+
teardown_test_environment()
370+
_django_instrumentor.uninstrument()
371+
372+
async def test_tracer_provider_traced(self):
373+
await self.async_client.post("/traced/")
374+
375+
spans = self.exporter.get_finished_spans()
376+
self.assertEqual(len(spans), 1)
377+
378+
span = spans[0]
379+
380+
self.assertEqual(
381+
span.resource.attributes["resource-key"], "resource-value"
382+
)

0 commit comments

Comments
 (0)
Please sign in to comment.