diff --git a/instana/instrumentation/django/middleware.py b/instana/instrumentation/django/middleware.py index 3c26d19d7..964a16705 100644 --- a/instana/instrumentation/django/middleware.py +++ b/instana/instrumentation/django/middleware.py @@ -29,6 +29,21 @@ def __init__(self, get_response=None): super(InstanaMiddleware, self).__init__(get_response) self.get_response = get_response + def _extract_custom_headers(self, span, headers, format): + if agent.options.extra_http_headers is None: + return + + try: + for custom_header in agent.options.extra_http_headers: + # Headers are available in this format: HTTP_X_CAPTURE_THIS + django_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if format else custom_header + + if django_header in headers: + span.set_tag("http.header.%s" % custom_header, headers[django_header]) + + except Exception: + logger.debug("extract_custom_headers: ", exc_info=True) + def process_request(self, request): try: env = request.environ @@ -36,12 +51,7 @@ def process_request(self, request): ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) request.iscope = tracer.start_active_span('django', child_of=ctx) - if agent.options.extra_http_headers is not None: - for custom_header in agent.options.extra_http_headers: - # Headers are available in this format: HTTP_X_CAPTURE_THIS - django_header = ('HTTP_' + custom_header.upper()).replace('-', '_') - if django_header in env: - request.iscope.span.set_tag("http.header.%s" % custom_header, env[django_header]) + self._extract_custom_headers(request.iscope.span, env, format=True) request.iscope.span.set_tag(ext.HTTP_METHOD, request.method) if 'PATH_INFO' in env: @@ -75,7 +85,9 @@ def process_response(self, request, response): path_tpl = None if path_tpl: request.iscope.span.set_tag("http.path_tpl", path_tpl) + request.iscope.span.set_tag(ext.HTTP_STATUS_CODE, response.status_code) + self._extract_custom_headers(request.iscope.span, response.headers, format=False) tracer.inject(request.iscope.span.context, ot.Format.HTTP_HEADERS, response) response['Server-Timing'] = "intid;desc=%s" % request.iscope.span.context.trace_id except Exception: diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index 3c0afd0f4..b8a3e58b0 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -125,10 +125,19 @@ def complex(request): return HttpResponse('Stan wuz here!') +def response_with_headers(request): + headers = { + 'X-Capture-This-Too': 'this too', + 'X-Capture-That-Too': 'that too' + } + return HttpResponse('Stan wuz here with headers!', headers=headers) + + urlpatterns = [ re_path(r'^$', index, name='index'), re_path(r'^cause_error$', cause_error, name='cause_error'), re_path(r'^another$', another), re_path(r'^not_found$', not_found, name='not_found'), - re_path(r'^complex$', complex, name='complex') + re_path(r'^complex$', complex, name='complex'), + re_path(r'^response_with_headers$', response_with_headers, name='response_with_headers') ] diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index 5be8c4b87..9a471faad 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -2,14 +2,14 @@ # (c) Copyright Instana Inc. 2020 from __future__ import absolute_import +import os import urllib3 from django.apps import apps -from ..apps.app_django import INSTALLED_APPS from django.contrib.staticfiles.testing import StaticLiveServerTestCase -import os -from instana.singletons import agent, tracer +from ..apps.app_django import INSTALLED_APPS +from instana.singletons import agent, tracer from ..helpers import fail_with_message_and_span_dump, get_first_span_by_filter, drop_log_spans_from_list apps.populate(INSTALLED_APPS) @@ -30,7 +30,7 @@ def test_basic_request(self): with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/', fields={"test": 1}) - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -40,19 +40,19 @@ def test_basic_request(self): urllib3_span = spans[1] django_span = spans[0] - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing']) self.assertEqual("test", test_span.data["sdk"]["name"]) @@ -86,7 +86,7 @@ def test_synthetic_request(self): with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/', headers=headers) - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -106,7 +106,7 @@ def test_request_with_error(self): with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/cause_error') - assert response + self.assertTrue(response) self.assertEqual(500, response.status) spans = self.recorder.queued_spans() @@ -119,29 +119,29 @@ def test_request_with_error(self): filter = lambda span: span.n == 'sdk' and span.data['sdk']['name'] == 'test' test_span = get_first_span_by_filter(spans, filter) - assert (test_span) + self.assertTrue(test_span) filter = lambda span: span.n == 'urllib3' urllib3_span = get_first_span_by_filter(spans, filter) - assert (urllib3_span) + self.assertTrue(urllib3_span) filter = lambda span: span.n == 'django' django_span = get_first_span_by_filter(spans, filter) - assert (django_span) + self.assertTrue(django_span) - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing']) self.assertEqual("test", test_span.data["sdk"]["name"]) @@ -167,7 +167,7 @@ def test_request_with_not_found(self): with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/not_found') - assert response + self.assertTrue(response) self.assertEqual(404, response.status) spans = self.recorder.queued_spans() @@ -180,7 +180,7 @@ def test_request_with_not_found(self): filter = lambda span: span.n == 'django' django_span = get_first_span_by_filter(spans, filter) - assert (django_span) + self.assertTrue(django_span) self.assertIsNone(django_span.ec) self.assertEqual(404, django_span.data["http"]["status"]) @@ -189,7 +189,7 @@ def test_request_with_not_found_no_route(self): with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/no_route') - assert response + self.assertTrue(response) self.assertEqual(404, response.status) spans = self.recorder.queued_spans() @@ -202,7 +202,7 @@ def test_request_with_not_found_no_route(self): filter = lambda span: span.n == 'django' django_span = get_first_span_by_filter(spans, filter) - assert (django_span) + self.assertTrue(django_span) self.assertIsNone(django_span.data["http"]["path_tpl"]) self.assertIsNone(django_span.ec) self.assertEqual(404, django_span.data["http"]["status"]) @@ -211,7 +211,7 @@ def test_complex_request(self): with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/complex') - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() self.assertEqual(5, len(spans)) @@ -222,19 +222,19 @@ def test_complex_request(self): ot_span1 = spans[1] ot_span2 = spans[0] - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing']) self.assertEqual("test", test_span.data["sdk"]["name"]) @@ -261,19 +261,21 @@ def test_complex_request(self): self.assertEqual(200, django_span.data["http"]["status"]) self.assertEqual('^complex$', django_span.data["http"]["path_tpl"]) - def test_custom_header_capture(self): + def test_request_header_capture(self): # Hack together a manual custom headers list + original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] - request_headers = dict() - request_headers['X-Capture-This'] = 'this' - request_headers['X-Capture-That'] = 'that' + request_headers = { + 'X-Capture-This': 'this', + 'X-Capture-That': 'that' + } with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) # response = self.client.get('/') - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -301,11 +303,56 @@ def test_custom_header_capture(self): self.assertEqual(200, django_span.data["http"]["status"]) self.assertEqual('^$', django_span.data["http"]["path_tpl"]) - assert "X-Capture-This" in django_span.data["http"]["header"] + self.assertIn("X-Capture-This", django_span.data["http"]["header"]) self.assertEqual("this", django_span.data["http"]["header"]["X-Capture-This"]) - assert "X-Capture-That" in django_span.data["http"]["header"] + self.assertIn("X-Capture-That", django_span.data["http"]["header"]) self.assertEqual("that", django_span.data["http"]["header"]["X-Capture-That"]) + agent.options.extra_http_headers = original_extra_http_headers + + def test_response_header_capture(self): + # Hack together a manual custom headers list + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = [u'X-Capture-This-Too', u'X-Capture-That-Too'] + + with tracer.start_active_span('test'): + response = self.http.request('GET', self.live_server_url + '/response_with_headers') + + self.assertTrue(response) + self.assertEqual(200, response.status) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + test_span = spans[2] + urllib3_span = spans[1] + django_span = spans[0] + + self.assertEqual("test", test_span.data["sdk"]["name"]) + self.assertEqual("urllib3", urllib3_span.n) + self.assertEqual("django", django_span.n) + + self.assertEqual(test_span.t, urllib3_span.t) + self.assertEqual(urllib3_span.t, django_span.t) + + self.assertEqual(urllib3_span.p, test_span.s) + self.assertEqual(django_span.p, urllib3_span.s) + + self.assertEqual(None, django_span.ec) + self.assertIsNone(django_span.stack) + + self.assertEqual('/response_with_headers', django_span.data["http"]["url"]) + self.assertEqual('GET', django_span.data["http"]["method"]) + self.assertEqual(200, django_span.data["http"]["status"]) + self.assertEqual('^response_with_headers$', django_span.data["http"]["path_tpl"]) + + self.assertIn("X-Capture-This-Too", django_span.data["http"]["header"]) + self.assertEqual("this too", django_span.data["http"]["header"]["X-Capture-This-Too"]) + self.assertIn("X-Capture-That-Too", django_span.data["http"]["header"]) + self.assertEqual("that too", django_span.data["http"]["header"]["X-Capture-That-Too"]) + + agent.options.extra_http_headers = original_extra_http_headers + def test_with_incoming_context(self): request_headers = dict() request_headers['X-INSTANA-T'] = '1' @@ -315,7 +362,7 @@ def test_with_incoming_context(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -326,29 +373,29 @@ def test_with_incoming_context(self): self.assertEqual(django_span.t, '0000000000000001') self.assertEqual(django_span.p, '0000000000000001') - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) - assert ('traceparent' in response.headers) + self.assertIn('traceparent', response.headers) # The incoming traceparent header had version 01 (which does not exist at the time of writing), but since we # support version 00, we also need to pass down 00 for the version field. self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), response.headers['traceparent']) - assert ('tracestate' in response.headers) + self.assertIn('tracestate', response.headers) self.assertEqual( 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( django_span.t, django_span.s), response.headers['tracestate']) server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing']) def test_with_incoming_context_and_correlation(self): @@ -361,7 +408,7 @@ def test_with_incoming_context_and_correlation(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -378,27 +425,28 @@ def test_with_incoming_context_and_correlation(self): self.assertEqual(django_span.crtp, 'web') self.assertEqual(django_span.crid, '1234567890abcdef') - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) - assert ('traceparent' in response.headers) + self.assertIn('traceparent', response.headers) self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), response.headers['traceparent']) - assert ('tracestate' in response.headers) + self.assertIn('tracestate', response.headers) self.assertEqual( 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( django_span.t, django_span.s), response.headers['tracestate']) + server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing']) def test_with_incoming_traceparent_tracestate(self): @@ -408,7 +456,7 @@ def test_with_incoming_traceparent_tracestate(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -423,28 +471,28 @@ def test_with_incoming_traceparent_tracestate(self): self.assertEqual(django_span.lt, '4bf92f3577b34da6a3ce929d0e0e4736') self.assertEqual(django_span.tp, True) - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) - assert ('traceparent' in response.headers) + self.assertIn('traceparent', response.headers) self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), response.headers['traceparent']) - assert ('tracestate' in response.headers) + self.assertIn('tracestate', response.headers) self.assertEqual( 'in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( django_span.s), response.headers['tracestate']) server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing']) def test_with_incoming_traceparent_tracestate_disable_traceparent(self): @@ -455,7 +503,7 @@ def test_with_incoming_traceparent_tracestate_disable_traceparent(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -466,28 +514,28 @@ def test_with_incoming_traceparent_tracestate_disable_traceparent(self): self.assertEqual(django_span.t, 'a3ce929d0e0e4736') # last 16 chars from traceparent trace_id self.assertEqual(django_span.p, '8357ccd9da194656') - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) - assert ('traceparent' in response.headers) + self.assertIn('traceparent', response.headers) self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), response.headers['traceparent']) - assert ('tracestate' in response.headers) + self.assertIn('tracestate', response.headers) self.assertEqual( - 'in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.s), response.headers['tracestate']) + 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( + django_span.t, django_span.s), response.headers['tracestate']) server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing']) def test_with_incoming_mixed_case_context(self): @@ -497,7 +545,7 @@ def test_with_incoming_mixed_case_context(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - assert response + self.assertTrue(response) self.assertEqual(200, response.status) spans = self.recorder.queued_spans() @@ -508,17 +556,17 @@ def test_with_incoming_mixed_case_context(self): self.assertEqual(django_span.t, '0000000000000001') self.assertEqual(django_span.p, '0000000000000001') - assert ('X-INSTANA-T' in response.headers) - assert (int(response.headers['X-INSTANA-T'], 16)) + self.assertIn('X-INSTANA-T', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) - assert ('X-INSTANA-S' in response.headers) - assert (int(response.headers['X-INSTANA-S'], 16)) + self.assertIn('X-INSTANA-S', response.headers) + self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) - assert ('X-INSTANA-L' in response.headers) + self.assertIn('X-INSTANA-L', response.headers) self.assertEqual('1', response.headers['X-INSTANA-L']) server_timing_value = "intid;desc=%s" % django_span.t - assert ('Server-Timing' in response.headers) + self.assertIn('Server-Timing', response.headers) self.assertEqual(server_timing_value, response.headers['Server-Timing'])