16
16
from unittest .mock import Mock , patch
17
17
18
18
from django import VERSION , conf
19
+ from django .conf .urls import url
20
+ from django .http import HttpRequest , HttpResponse
19
21
from django .test import SimpleTestCase
20
22
from django .test .utils import setup_test_environment , teardown_test_environment
21
23
from django .urls import re_path
22
24
import pytest
23
25
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
25
37
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
+ )
27
44
from opentelemetry .util .http import get_excluded_urls , get_traced_request_attrs
28
45
29
46
# pylint: disable=import-error
62
79
class TestMiddlewareAsgi (SimpleTestCase , TestBase ):
63
80
@classmethod
64
81
def setUpClass (cls ):
65
- super ().setUpClass ()
66
82
conf .settings .configure (ROOT_URLCONF = modules [__name__ ])
83
+ super ().setUpClass ()
67
84
68
85
def setUp (self ):
69
86
super ().setUp ()
@@ -101,16 +118,6 @@ def tearDownClass(cls):
101
118
super ().tearDownClass ()
102
119
conf .settings = conf .LazySettings ()
103
120
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
-
114
121
async def test_templated_route_get (self ):
115
122
await self .async_client .get ("/route/2020/template/" )
116
123
@@ -122,19 +129,17 @@ async def test_templated_route_get(self):
122
129
self .assertEqual (span .name , "^route/(?P<year>[0-9]{4})/template/$" )
123
130
self .assertEqual (span .kind , SpanKind .SERVER )
124
131
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" )
126
133
self .assertEqual (
127
- span .attributes ["http.url" ],
134
+ span .attributes [SpanAttributes . HTTP_URL ],
128
135
"http://127.0.0.1/route/2020/template/" ,
129
136
)
130
137
self .assertEqual (
131
- span .attributes ["http.route" ],
138
+ span .attributes [SpanAttributes . HTTP_ROUTE ],
132
139
"^route/(?P<year>[0-9]{4})/template/$" ,
133
140
)
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 )
138
143
139
144
async def test_traced_get (self ):
140
145
await self .async_client .get ("/traced/" )
@@ -147,15 +152,16 @@ async def test_traced_get(self):
147
152
self .assertEqual (span .name , "^traced/" )
148
153
self .assertEqual (span .kind , SpanKind .SERVER )
149
154
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
+ )
151
160
self .assertEqual (
152
- span .attributes ["http.url" ], "http://127.0.0.1/ traced/"
161
+ span .attributes [SpanAttributes . HTTP_ROUTE ], "^ traced/"
153
162
)
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 )
159
165
160
166
async def test_not_recording (self ):
161
167
mock_tracer = Mock ()
@@ -181,15 +187,16 @@ async def test_traced_post(self):
181
187
self .assertEqual (span .name , "^traced/" )
182
188
self .assertEqual (span .kind , SpanKind .SERVER )
183
189
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
+ )
185
195
self .assertEqual (
186
- span .attributes ["http.url" ], "http://127.0.0.1/ traced/"
196
+ span .attributes [SpanAttributes . HTTP_ROUTE ], "^ traced/"
187
197
)
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 )
193
200
194
201
async def test_error (self ):
195
202
with self .assertRaises (ValueError ):
@@ -203,19 +210,24 @@ async def test_error(self):
203
210
self .assertEqual (span .name , "^error/" )
204
211
self .assertEqual (span .kind , SpanKind .SERVER )
205
212
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" )
207
214
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/" ,
209
217
)
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 )
213
221
214
222
self .assertEqual (len (span .events ), 1 )
215
223
event = span .events [0 ]
216
224
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
+ )
219
231
220
232
async def test_exclude_lists (self ):
221
233
await self .async_client .get ("/excluded_arg/123" )
@@ -262,9 +274,6 @@ async def test_span_name_404(self):
262
274
span = span_list [0 ]
263
275
self .assertEqual (span .name , "HTTP GET" )
264
276
265
- @pytest .mark .skip (
266
- reason = "TODO: Traced request attributes not supported yet"
267
- )
268
277
async def test_traced_request_attrs (self ):
269
278
await self .async_client .get ("/span_name/1234/" , CONTENT_TYPE = "test/ct" )
270
279
span_list = self .memory_exporter .get_finished_spans ()
@@ -274,3 +283,100 @@ async def test_traced_request_attrs(self):
274
283
self .assertEqual (span .attributes ["path_info" ], "/span_name/1234/" )
275
284
self .assertEqual (span .attributes ["content_type" ], "test/ct" )
276
285
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