Skip to content

Commit f985bd2

Browse files
authored
Django add path templating support (#310)
* adding path templating feature for django
1 parent 94fd99b commit f985bd2

File tree

4 files changed

+85
-48
lines changed

4 files changed

+85
-48
lines changed

instana/instrumentation/django/middleware.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
class InstanaMiddleware(MiddlewareMixin):
2626
""" Django Middleware to provide request tracing for Instana """
27+
2728
def __init__(self, get_response=None):
2829
super(InstanaMiddleware, self).__init__(get_response)
2930
self.get_response = get_response
@@ -46,7 +47,8 @@ def process_request(self, request):
4647
if 'PATH_INFO' in env:
4748
request.iscope.span.set_tag(ext.HTTP_URL, env['PATH_INFO'])
4849
if 'QUERY_STRING' in env and len(env['QUERY_STRING']):
49-
scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list)
50+
scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher,
51+
agent.options.secrets_list)
5052
request.iscope.span.set_tag("http.params", scrubbed_params)
5153
if 'HTTP_HOST' in env:
5254
request.iscope.span.set_tag("http.host", env['HTTP_HOST'])
@@ -58,7 +60,21 @@ def process_response(self, request, response):
5860
if request.iscope is not None:
5961
if 500 <= response.status_code <= 511:
6062
request.iscope.span.assure_errored()
61-
63+
# for django >= 2.2
64+
if request.resolver_match is not None and hasattr(request.resolver_match, 'route'):
65+
path_tpl = request.resolver_match.route
66+
# django < 2.2 or in case of 404
67+
else:
68+
try:
69+
from django.urls import resolve
70+
view_name = resolve(request.path)._func_path
71+
path_tpl = "".join(self.__url_pattern_route(view_name))
72+
except Exception:
73+
# the resolve method can fire a Resolver404 exception, in this case there is no matching route
74+
# so the path_tpl is set to None in order not to be added as a tag
75+
path_tpl = None
76+
if path_tpl:
77+
request.iscope.span.set_tag("http.path_tpl", path_tpl)
6278
request.iscope.span.set_tag(ext.HTTP_STATUS_CODE, response.status_code)
6379
tracer.inject(request.iscope.span.context, ot.Format.HTTP_HEADERS, response)
6480
response['Server-Timing'] = "intid;desc=%s" % request.iscope.span.context.trace_id
@@ -79,6 +95,37 @@ def process_exception(self, request, exception):
7995
if request.iscope is not None:
8096
request.iscope.span.log_exception(exception)
8197

98+
def __url_pattern_route(self, view_name):
99+
from django.conf import settings
100+
try:
101+
from django.urls import RegexURLPattern as URLPattern
102+
from django.urls import RegexURLResolver as URLResolver
103+
except ImportError:
104+
from django.urls import URLPattern, URLResolver
105+
106+
urlconf = __import__(settings.ROOT_URLCONF, {}, {}, [''])
107+
108+
def list_urls(urlpatterns, parent_pattern=None):
109+
if not urlpatterns:
110+
return
111+
if parent_pattern is None:
112+
parent_pattern = []
113+
first = urlpatterns[0]
114+
if isinstance(first, URLPattern):
115+
if first.lookup_str == view_name:
116+
if hasattr(first, "regex"):
117+
return parent_pattern + [str(first.regex.pattern)]
118+
else:
119+
return parent_pattern + [str(first.pattern)]
120+
elif isinstance(first, URLResolver):
121+
if hasattr(first, "regex"):
122+
return list_urls(first.url_patterns, parent_pattern + [str(first.regex.pattern)])
123+
else:
124+
return list_urls(first.url_patterns, parent_pattern + [str(first.pattern)])
125+
return list_urls(urlpatterns[1:], parent_pattern)
126+
127+
return list_urls(urlconf.urlpatterns)
128+
82129

83130
def load_middleware_wrapper(wrapped, instance, args, kwargs):
84131
try:

tests/clients/test_asynqp.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -109,45 +109,6 @@ def test():
109109
self.assertTrue(type(rabbitmq_span.stack) is list)
110110
self.assertGreater(len(rabbitmq_span.stack), 0)
111111

112-
def test_publish_alternative(self):
113-
@asyncio.coroutine
114-
def test():
115-
with async_tracer.start_active_span('test'):
116-
msg = asynqp.Message({'hello': 'world'}, content_type='application/json')
117-
self.exchange.publish(msg, routing_key='routing.key')
118-
119-
self.loop.run_until_complete(test())
120-
121-
spans = self.recorder.queued_spans()
122-
self.assertEqual(2, len(spans))
123-
124-
rabbitmq_span = spans[0]
125-
test_span = spans[1]
126-
127-
self.assertIsNone(async_tracer.active_span)
128-
129-
# Same traceId
130-
self.assertEqual(test_span.t, rabbitmq_span.t)
131-
132-
# Parent relationships
133-
self.assertEqual(rabbitmq_span.p, test_span.s)
134-
135-
# Error logging
136-
self.assertIsNone(test_span.ec)
137-
self.assertIsNone(rabbitmq_span.ec)
138-
139-
# Span type
140-
self.assertEqual(rabbitmq_span.k, 2) # exit
141-
142-
# Rabbitmq
143-
self.assertEqual('test.exchange', rabbitmq_span.data["rabbitmq"]["exchange"])
144-
self.assertEqual('publish', rabbitmq_span.data["rabbitmq"]["sort"])
145-
self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"])
146-
self.assertEqual('routing.key', rabbitmq_span.data["rabbitmq"]["key"])
147-
self.assertIsNotNone(rabbitmq_span.stack)
148-
self.assertTrue(type(rabbitmq_span.stack) is list)
149-
self.assertGreater(len(rabbitmq_span.stack), 0)
150-
151112
def test_many_publishes(self):
152113
@asyncio.coroutine
153114
def test():

tests/frameworks/test_django.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def tearDown(self):
2828

2929
def test_basic_request(self):
3030
with tracer.start_active_span('test'):
31-
response = self.http.request('GET', self.live_server_url + '/')
31+
response = self.http.request('GET', self.live_server_url + '/', fields={"test": 1})
3232

3333
assert response
3434
self.assertEqual(200, response.status)
@@ -70,10 +70,12 @@ def test_basic_request(self):
7070
self.assertIsNone(test_span.sy)
7171

7272
self.assertEqual(None, django_span.ec)
73-
7473
self.assertEqual('/', django_span.data["http"]["url"])
7574
self.assertEqual('GET', django_span.data["http"]["method"])
7675
self.assertEqual(200, django_span.data["http"]["status"])
76+
self.assertEqual('test=1', django_span.data["http"]["params"])
77+
self.assertEqual('^$', django_span.data["http"]["path_tpl"])
78+
7779
self.assertIsNone(django_span.stack)
7880

7981
def test_synthetic_request(self):
@@ -94,6 +96,8 @@ def test_synthetic_request(self):
9496
urllib3_span = spans[1]
9597
django_span = spans[0]
9698

99+
self.assertEqual('^$', django_span.data["http"]["path_tpl"])
100+
97101
self.assertTrue(django_span.sy)
98102
self.assertIsNone(urllib3_span.sy)
99103
self.assertIsNone(test_span.sy)
@@ -115,15 +119,15 @@ def test_request_with_error(self):
115119

116120
filter = lambda span: span.n == 'sdk' and span.data['sdk']['name'] == 'test'
117121
test_span = get_first_span_by_filter(spans, filter)
118-
assert(test_span)
122+
assert (test_span)
119123

120124
filter = lambda span: span.n == 'urllib3'
121125
urllib3_span = get_first_span_by_filter(spans, filter)
122-
assert(urllib3_span)
126+
assert (urllib3_span)
123127

124128
filter = lambda span: span.n == 'django'
125129
django_span = get_first_span_by_filter(spans, filter)
126-
assert(django_span)
130+
assert (django_span)
127131

128132
assert ('X-INSTANA-T' in response.headers)
129133
assert (int(response.headers['X-INSTANA-T'], 16))
@@ -156,6 +160,7 @@ def test_request_with_error(self):
156160
self.assertEqual('GET', django_span.data["http"]["method"])
157161
self.assertEqual(500, django_span.data["http"]["status"])
158162
self.assertEqual('This is a fake error: /cause-error', django_span.data["http"]["error"])
163+
self.assertEqual('^cause_error$', django_span.data["http"]["path_tpl"])
159164
self.assertIsNone(django_span.stack)
160165

161166
def test_request_with_not_found(self):
@@ -175,8 +180,30 @@ def test_request_with_not_found(self):
175180

176181
filter = lambda span: span.n == 'django'
177182
django_span = get_first_span_by_filter(spans, filter)
178-
assert(django_span)
183+
assert (django_span)
184+
185+
self.assertIsNone(django_span.ec)
186+
self.assertEqual(404, django_span.data["http"]["status"])
187+
188+
def test_request_with_not_found_no_route(self):
189+
with tracer.start_active_span('test'):
190+
response = self.http.request('GET', self.live_server_url + '/no_route')
191+
192+
assert response
193+
self.assertEqual(404, response.status)
179194

195+
spans = self.recorder.queued_spans()
196+
spans = drop_log_spans_from_list(spans)
197+
198+
span_count = len(spans)
199+
if span_count != 3:
200+
msg = "Expected 3 spans but got %d" % span_count
201+
fail_with_message_and_span_dump(msg, spans)
202+
203+
filter = lambda span: span.n == 'django'
204+
django_span = get_first_span_by_filter(spans, filter)
205+
assert (django_span)
206+
self.assertIsNone(django_span.data["http"]["path_tpl"])
180207
self.assertIsNone(django_span.ec)
181208
self.assertEqual(404, django_span.data["http"]["status"])
182209

@@ -232,6 +259,7 @@ def test_complex_request(self):
232259
self.assertEqual('/complex', django_span.data["http"]["url"])
233260
self.assertEqual('GET', django_span.data["http"]["method"])
234261
self.assertEqual(200, django_span.data["http"]["status"])
262+
self.assertEqual('^complex$', django_span.data["http"]["path_tpl"])
235263

236264
def test_custom_header_capture(self):
237265
# Hack together a manual custom headers list
@@ -271,6 +299,7 @@ def test_custom_header_capture(self):
271299
self.assertEqual('/', django_span.data["http"]["url"])
272300
self.assertEqual('GET', django_span.data["http"]["method"])
273301
self.assertEqual(200, django_span.data["http"]["status"])
302+
self.assertEqual('^$', django_span.data["http"]["path_tpl"])
274303

275304
assert "X-Capture-This" in django_span.data["http"]["header"]
276305
self.assertEqual("this", django_span.data["http"]["header"]["X-Capture-This"])

tests/requirements-27.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ aiohttp>=3.5.4;python_version>="3.5"
33
asynqp>=0.4;python_version>="3.5"
44
boto3>=1.10.0
55
celery>=4.1.1
6-
django<2.0.0
6+
django>=1.11,<2.0.0
77
fastapi>=0.61.1;python_version>="3.6"
88
flask>=0.12.2
99
grpcio>=1.18.0

0 commit comments

Comments
 (0)