diff --git a/contrib/opencensus-ext-django/opencensus/ext/django/middleware.py b/contrib/opencensus-ext-django/opencensus/ext/django/middleware.py index e48bf814b..34354866d 100644 --- a/contrib/opencensus-ext-django/opencensus/ext/django/middleware.py +++ b/contrib/opencensus-ext-django/opencensus/ext/django/middleware.py @@ -17,7 +17,6 @@ import six import django.conf -from django.utils.deprecation import MiddlewareMixin from opencensus.common import configuration from opencensus.trace import attributes_helper @@ -33,9 +32,6 @@ HTTP_URL = attributes_helper.COMMON_ATTRIBUTES['HTTP_URL'] HTTP_STATUS_CODE = attributes_helper.COMMON_ATTRIBUTES['HTTP_STATUS_CODE'] -REQUEST_THREAD_LOCAL_KEY = 'django_request' -SPAN_THREAD_LOCAL_KEY = 'django_span' - BLACKLIST_PATHS = 'BLACKLIST_PATHS' BLACKLIST_HOSTNAMES = 'BLACKLIST_HOSTNAMES' @@ -48,59 +44,17 @@ class _DjangoMetaWrapper(object): Django request.META """ - def __init__(self, meta=None): - self.meta = meta or _get_django_request().META + def __init__(self, meta): + self.meta = meta def get(self, key): return self.meta.get('HTTP_' + key.upper().replace('-', '_')) -def _get_django_request(): - """Get Django request from thread local. - - :rtype: str - :returns: Django request. - """ - return execution_context.get_opencensus_attr(REQUEST_THREAD_LOCAL_KEY) - - -def _get_django_span(): - """Get Django span from thread local. - - :rtype: str - :returns: Django request. - """ - return execution_context.get_opencensus_attr(SPAN_THREAD_LOCAL_KEY) - - -def _get_current_tracer(): - """Get the current request tracer.""" - return execution_context.get_opencensus_tracer() - - -def _set_django_attributes(span, request): - """Set the django related attributes.""" - django_user = getattr(request, 'user', None) - - if django_user is None: - return - - user_id = django_user.pk - user_name = django_user.get_username() - - # User id is the django autofield for User model as the primary key - if user_id is not None: - span.add_attribute('django.user.id', str(user_id)) - - if user_name is not None: - span.add_attribute('django.user.name', str(user_name)) - - -class OpencensusMiddleware(MiddlewareMixin): - """Saves the request in thread local""" - - def __init__(self, get_response=None): +class OpencensusMiddleware(object): + def __init__(self, get_response): self.get_response = get_response + settings = getattr(django.conf.settings, 'OPENCENSUS', {}) settings = settings.get('TRACE', {}) @@ -123,94 +77,80 @@ def __init__(self, get_response=None): self.blacklist_hostnames = settings.get(BLACKLIST_HOSTNAMES, None) - def process_request(self, request): - """Called on each request, before Django decides which view to execute. - + def __call__(self, request): + """ :type request: :class:`~django.http.request.HttpRequest` :param request: Django http request. """ - # Do not trace if the url is blacklisted - if utils.disable_tracing_url(request.path, self.blacklist_paths): - return + span = None - # Add the request to thread local - execution_context.set_opencensus_attr( - REQUEST_THREAD_LOCAL_KEY, - request) - - execution_context.set_opencensus_attr( - 'blacklist_hostnames', - self.blacklist_hostnames) - - try: - # Start tracing this request - span_context = self.propagator.from_headers( - _DjangoMetaWrapper(_get_django_request().META)) - - # Reload the tracer with the new span context - tracer = tracer_module.Tracer( - span_context=span_context, - sampler=self.sampler, - exporter=self.exporter, - propagator=self.propagator) - - # Span name is being set at process_view - span = tracer.start_span() - span.span_kind = span_module.SpanKind.SERVER - tracer.add_attribute_to_current_span( - attribute_key=HTTP_METHOD, - attribute_value=request.method) - tracer.add_attribute_to_current_span( - attribute_key=HTTP_URL, - attribute_value=str(request.path)) - - # Add the span to thread local - # in some cases (exceptions, timeouts) currentspan in - # response event will be one of a child spans. - # let's keep reference to 'django' span and - # use it in response event + # Do not trace if the url is blacklisted + if not utils.disable_tracing_url(request.path, self.blacklist_paths): + # Add the request to thread local execution_context.set_opencensus_attr( - SPAN_THREAD_LOCAL_KEY, - span) - - except Exception: # pragma: NO COVER - log.error('Failed to trace request', exc_info=True) - - def process_view(self, request, view_func, *args, **kwargs): - """Process view is executed before the view function, here we get the - function name add set it as the span name. + 'blacklist_hostnames', + self.blacklist_hostnames) + + try: + # Start tracing this request + span_context = self.propagator.from_headers( + _DjangoMetaWrapper(request.META)) + + # Reload the tracer with the new span context + tracer = tracer_module.Tracer( + span_context=span_context, + sampler=self.sampler, + exporter=self.exporter, + propagator=self.propagator) + + # Span name is being set at process_view + span = tracer.start_span() + span.span_kind = span_module.SpanKind.SERVER + tracer.add_attribute_to_current_span( + attribute_key=HTTP_METHOD, + attribute_value=request.method) + tracer.add_attribute_to_current_span( + attribute_key=HTTP_URL, + attribute_value=request.path) + + except Exception: # pragma: NO COVER + log.error('Failed to trace request', exc_info=True) + + response = self.get_response(request) + + if span: + try: + span.name = utils.get_func_name(request.resolver_match.func) + + span.add_attribute( + attribute_key=HTTP_STATUS_CODE, + attribute_value=str(response.status_code)) + + self.set_django_attributes(span, request, response) + + tracer.end_span() + tracer.finish() + except Exception: # pragma: NO COVER + log.error('Failed to trace request', exc_info=True) + + return response + + def set_django_attributes(self, span, request, response): # noqa + """ The last method before the span finishes, allowing to set + django-related attributes on the span. Override this in a + subclass of the middleware to set custom attributes. """ + django_user = getattr(request, 'user', None) - # Do not trace if the url is blacklisted - if utils.disable_tracing_url(request.path, self.blacklist_paths): + if django_user is None: return - try: - # Get the current span and set the span name to the current - # function name of the request. - tracer = _get_current_tracer() - span = tracer.current_span() - span.name = utils.get_func_name(view_func) - except Exception: # pragma: NO COVER - log.error('Failed to trace request', exc_info=True) + user_id = django_user.pk + user_name = django_user.get_username() - def process_response(self, request, response): - # Do not trace if the url is blacklisted - if utils.disable_tracing_url(request.path, self.blacklist_paths): - return response - - try: - span = _get_django_span() - span.add_attribute( - attribute_key=HTTP_STATUS_CODE, - attribute_value=str(response.status_code)) - - _set_django_attributes(span, request) - - tracer = _get_current_tracer() - tracer.end_span() - tracer.finish() - except Exception: # pragma: NO COVER - log.error('Failed to trace request', exc_info=True) - finally: - return response + # User id is the django autofield for User model as the primary key + if user_id is not None: + span.add_attribute('django.user.id', str(user_id)) + + if user_name is not None: + span.add_attribute('django.user.name', str(user_name)) diff --git a/contrib/opencensus-ext-django/tests/test_django_middleware.py b/contrib/opencensus-ext-django/tests/test_django_middleware.py index e037b8fad..5e51b0f43 100644 --- a/contrib/opencensus-ext-django/tests/test_django_middleware.py +++ b/contrib/opencensus-ext-django/tests/test_django_middleware.py @@ -21,7 +21,6 @@ from opencensus.trace import execution_context from opencensus.trace import print_exporter from opencensus.trace import samplers -from opencensus.trace import span as span_module from opencensus.trace import utils from opencensus.trace.blank_span import BlankSpan from opencensus.trace.propagation import trace_context_http_header_format @@ -32,30 +31,51 @@ class TestOpencensusMiddleware(unittest.TestCase): def setUp(self): from django.conf import settings as django_settings from django.test.utils import setup_test_environment + from opencensus.ext.django import middleware + from django.http import HttpResponse + from django.views import View if not django_settings.configured: django_settings.configure() setup_test_environment() + self.middleware_kls = middleware.OpencensusMiddleware + + class MockViewError(View): + def get(self, *args, **kwargs): + return HttpResponse(status=500) + + def post(self, *args, **kwargs): + return HttpResponse(status=500) + + class MockViewOk(View): + def get(self, *args, **kwargs): + return HttpResponse(status=200) + + def post(self, *args, **kwargs): + return HttpResponse(status=200) + + self.view_func_error = MockViewError.as_view() + self.view_func_ok = MockViewOk.as_view() + def tearDown(self): execution_context.clear() teardown_test_environment() def test_constructor_default(self): - from opencensus.ext.django import middleware - - middleware = middleware.OpencensusMiddleware() + middleware_obj = self.middleware_kls(mock.Mock()) - assert isinstance(middleware.sampler, samplers.ProbabilitySampler) - assert isinstance(middleware.exporter, print_exporter.PrintExporter) + assert isinstance(middleware_obj.sampler, samplers.ProbabilitySampler) assert isinstance( - middleware.propagator, + middleware_obj.exporter, + print_exporter.PrintExporter + ) + assert isinstance( + middleware_obj.propagator, trace_context_http_header_format.TraceContextPropagator, ) def test_configuration(self): - from opencensus.ext.django import middleware - settings = type('Test', (object,), {}) settings.OPENCENSUS = { 'TRACE': { @@ -69,66 +89,19 @@ def test_configuration(self): settings) with patch_settings: - middleware = middleware.OpencensusMiddleware() + middleware_obj = self.middleware_kls(mock.Mock()) - assert isinstance(middleware.sampler, samplers.AlwaysOnSampler) - assert isinstance(middleware.exporter, print_exporter.PrintExporter) + assert isinstance(middleware_obj.sampler, samplers.AlwaysOnSampler) assert isinstance( - middleware.propagator, + middleware_obj.exporter, + print_exporter.PrintExporter + ) + assert isinstance( + middleware_obj.propagator, trace_context_http_header_format.TraceContextPropagator, ) - def test_process_request(self): - from opencensus.ext.django import middleware - - trace_id = '2dd43a1d6b2549c6bc2a1a54c2fc0b05' - span_id = '6e0c63257de34c92' - django_trace_id = '00-{}-{}-00'.format(trace_id, span_id) - - django_request = RequestFactory().get('/', **{ - 'HTTP_TRACEPARENT': django_trace_id}) - - # Force the test request to be sampled - settings = type('Test', (object,), {}) - settings.OPENCENSUS = { - 'TRACE': { - 'SAMPLER': 'opencensus.trace.samplers.AlwaysOnSampler()', # noqa - } - } - patch_settings = mock.patch( - 'django.conf.settings', - settings) - - with patch_settings: - middleware_obj = middleware.OpencensusMiddleware() - - # test process_request - middleware_obj.process_request(django_request) - - tracer = middleware._get_current_tracer() - - span = tracer.current_span() - - expected_attributes = { - 'http.url': u'/', - 'http.method': 'GET', - } - self.assertEqual(span.span_kind, span_module.SpanKind.SERVER) - self.assertEqual(span.attributes, expected_attributes) - self.assertEqual(span.parent_span.span_id, span_id) - - span_context = tracer.span_context - self.assertEqual(span_context.trace_id, trace_id) - - # test process_view - view_func = mock.Mock() - middleware_obj.process_view(django_request, view_func) - - self.assertEqual(span.name, 'mock.mock.Mock') - def test_blacklist_path(self): - from opencensus.ext.django import middleware - execution_context.clear() blacklist_paths = ['test_blacklist_path'] @@ -145,7 +118,7 @@ def test_blacklist_path(self): settings) with patch_settings: - middleware_obj = middleware.OpencensusMiddleware() + middleware_obj = self.middleware_kls(mock.Mock()) django_request = RequestFactory().get('/test_blacklist_path') disabled = utils.disable_tracing_url(django_request.path, @@ -153,33 +126,34 @@ def test_blacklist_path(self): self.assertTrue(disabled) self.assertEqual(middleware_obj.blacklist_paths, blacklist_paths) - # test process_request - middleware_obj.process_request(django_request) + # test processing request + middleware_obj(django_request) - tracer = middleware._get_current_tracer() + tracer = execution_context.get_opencensus_tracer() span = tracer.current_span() - # process view - view_func = mock.Mock() - middleware_obj.process_view(django_request, view_func) + assert isinstance(span, BlankSpan) - tracer = middleware._get_current_tracer() - span = tracer.current_span() - assert isinstance(span, BlankSpan) +class TestCustomOpencensusMiddleware(TestOpencensusMiddleware): - # process response - django_response = mock.Mock() - django_response.status_code = 200 + def setUp(self): + from opencensus.ext.django import middleware - middleware_obj.process_response(django_request, django_response) + super(TestCustomOpencensusMiddleware, self).setUp() - tracer = middleware._get_current_tracer() - span = tracer.current_span() - assert isinstance(span, BlankSpan) + class CustomOpencensusMiddleware(middleware.OpencensusMiddleware): + def set_django_attributes(self, span, request, response): + # For the purpose of span inspection, set it on the instance + super(CustomOpencensusMiddleware, self).set_django_attributes( + span, request, response + ) + self.span = span - def test_process_response(self): - from opencensus.ext.django import middleware + self.middleware_kls = CustomOpencensusMiddleware + + def test_span_attributes_get_200(self): + from django.urls.resolvers import ResolverMatch trace_id = '2dd43a1d6b2549c6bc2a1a54c2fc0b05' span_id = '6e0c63257de34c92' @@ -188,6 +162,8 @@ def test_process_response(self): django_request = RequestFactory().get('/', **{ 'traceparent': django_trace_id, }) + django_request.resolver_match = ResolverMatch( + self.view_func_ok, None, None) # Force the test request to be sampled settings = type('Test', (object,), {}) @@ -201,18 +177,13 @@ def test_process_response(self): settings) with patch_settings: - middleware_obj = middleware.OpencensusMiddleware() + middleware_obj = self.middleware_kls(self.view_func_ok) - middleware_obj.process_request(django_request) - tracer = middleware._get_current_tracer() - span = tracer.current_span() + tracer = execution_context.get_opencensus_tracer() exporter_mock = mock.Mock() tracer.exporter = exporter_mock - django_response = mock.Mock() - django_response.status_code = 200 - expected_attributes = { 'http.url': u'/', 'http.method': 'GET', @@ -226,20 +197,23 @@ def test_process_response(self): mock_user.get_username.return_value = 'test_name' django_request.user = mock_user - middleware_obj.process_response(django_request, django_response) + middleware_obj(django_request) + span = middleware_obj.span self.assertEqual(span.attributes, expected_attributes) - def test_process_response_unfinished_child_span(self): - from opencensus.ext.django import middleware + def test_span_attributes_post_500(self): + from django.urls.resolvers import ResolverMatch trace_id = '2dd43a1d6b2549c6bc2a1a54c2fc0b05' span_id = '6e0c63257de34c92' django_trace_id = '00-{}-{}-00'.format(trace_id, span_id) - django_request = RequestFactory().get('/', **{ + django_request = RequestFactory().post('/', **{ 'traceparent': django_trace_id, }) + django_request.resolver_match = ResolverMatch( + self.view_func_error, None, None) # Force the test request to be sampled settings = type('Test', (object,), {}) @@ -253,21 +227,16 @@ def test_process_response_unfinished_child_span(self): settings) with patch_settings: - middleware_obj = middleware.OpencensusMiddleware() + middleware_obj = self.middleware_kls(self.view_func_error) - middleware_obj.process_request(django_request) - tracer = middleware._get_current_tracer() - span = tracer.current_span() + tracer = execution_context.get_opencensus_tracer() exporter_mock = mock.Mock() tracer.exporter = exporter_mock - django_response = mock.Mock() - django_response.status_code = 500 - expected_attributes = { 'http.url': u'/', - 'http.method': 'GET', + 'http.method': 'POST', 'http.status_code': '500', 'django.user.id': '123', 'django.user.name': 'test_name' @@ -278,9 +247,8 @@ def test_process_response_unfinished_child_span(self): mock_user.get_username.return_value = 'test_name' django_request.user = mock_user - tracer.start_span() - self.assertNotEqual(span, tracer.current_span()) - middleware_obj.process_response(django_request, django_response) + middleware_obj(django_request) + span = middleware_obj.span self.assertEqual(span.attributes, expected_attributes) @@ -294,41 +262,43 @@ def add_attribute(self, key, value): self.attributes[key] = value def test__set_django_attributes_no_user(self): - from opencensus.ext.django.middleware import \ - _set_django_attributes + from opencensus.ext.django.middleware import OpencensusMiddleware span = self.Span() request = mock.Mock() + response = mock.Mock() request.user = None - _set_django_attributes(span, request) + OpencensusMiddleware(mock.Mock()).set_django_attributes( + span, request, response) expected_attributes = {} self.assertEqual(span.attributes, expected_attributes) def test__set_django_attributes_no_user_info(self): - from opencensus.ext.django.middleware import \ - _set_django_attributes + from opencensus.ext.django.middleware import OpencensusMiddleware span = self.Span() request = mock.Mock() + response = mock.Mock() django_user = mock.Mock() request.user = django_user django_user.pk = None django_user.get_username.return_value = None - _set_django_attributes(span, request) + OpencensusMiddleware(mock.Mock()).set_django_attributes( + span, request, response) expected_attributes = {} self.assertEqual(span.attributes, expected_attributes) def test__set_django_attributes_with_user_info(self): - from opencensus.ext.django.middleware import \ - _set_django_attributes + from opencensus.ext.django.middleware import OpencensusMiddleware span = self.Span() request = mock.Mock() + response = mock.Mock() django_user = mock.Mock() request.user = django_user @@ -337,7 +307,8 @@ def test__set_django_attributes_with_user_info(self): django_user.pk = test_id django_user.get_username.return_value = test_name - _set_django_attributes(span, request) + OpencensusMiddleware(mock.Mock()).set_django_attributes( + span, request, response) expected_attributes = { 'django.user.id': '123',