Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
# project
from .conf import settings
from .compat import user_is_authenticated, get_resolver
from .utils import get_request_uri

from ...constants import ANALYTICS_SAMPLE_RATE_KEY
from ...contrib import func_name
from ...ext import SpanTypes, http
from ...internal.logger import get_logger
from ...propagation.http import HTTPPropagator
from ...settings import config

# 3p
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings as django_settings
import django

try:
    from django.utils.deprecation import MiddlewareMixin
    MiddlewareClass = MiddlewareMixin
except ImportError:
    MiddlewareClass = object

log = get_logger(__name__)

EXCEPTION_MIDDLEWARE = 'ddtrace.contrib.django.TraceExceptionMiddleware'
TRACE_MIDDLEWARE = 'ddtrace.contrib.django.TraceMiddleware'
MIDDLEWARE = 'MIDDLEWARE'
MIDDLEWARE_CLASSES = 'MIDDLEWARE_CLASSES'

# Default views list available from:
#   https://github.com/django/django/blob/38e2fdadfd9952e751deed662edf4c496d238f28/django/views/defaults.py
# DEV: Django doesn't call `process_view` when falling back to one of these internal error handling views
# DEV: We only use these names when `span.resource == 'unknown'` and we have one of these status codes
_django_default_views = {
    400: 'django.views.defaults.bad_request',
    403: 'django.views.defaults.permission_denied',
    404: 'django.views.defaults.page_not_found',
    500: 'django.views.defaults.server_error',
}


def _analytics_enabled():
    return (
        (config.analytics_enabled and settings.ANALYTICS_ENABLED is not False) or
        settings.ANALYTICS_ENABLED is True
    ) and settings.ANALYTICS_SAMPLE_RATE is not None


def get_middleware_insertion_point():
    """Returns the attribute name and collection object for the Django middleware.

    If middleware cannot be found, returns None for the middleware collection.
    """
    middleware = getattr(django_settings, MIDDLEWARE, None)
    # Prioritise MIDDLEWARE over ..._CLASSES, but only in 1.10 and later.
    if middleware is not None and django.VERSION >= (1, 10):
        return MIDDLEWARE, middleware
    return MIDDLEWARE_CLASSES, getattr(django_settings, MIDDLEWARE_CLASSES, None)


def insert_trace_middleware():
    middleware_attribute, middleware = get_middleware_insertion_point()
    if middleware is not None and TRACE_MIDDLEWARE not in set(middleware):
        setattr(django_settings, middleware_attribute, type(middleware)((TRACE_MIDDLEWARE,)) + middleware)


def remove_trace_middleware():
    _, middleware = get_middleware_insertion_point()
    if middleware and TRACE_MIDDLEWARE in set(middleware):
        middleware.remove(TRACE_MIDDLEWARE)


def insert_exception_middleware():
    middleware_attribute, middleware = get_middleware_insertion_point()
    if middleware is not None and EXCEPTION_MIDDLEWARE not in set(middleware):
        setattr(django_settings, middleware_attribute, middleware + type(middleware)((EXCEPTION_MIDDLEWARE,)))


def remove_exception_middleware():
    _, middleware = get_middleware_insertion_point()
    if middleware and EXCEPTION_MIDDLEWARE in set(middleware):
        middleware.remove(EXCEPTION_MIDDLEWARE)


class InstrumentationMixin(MiddlewareClass):
    """
    Useful mixin base class for tracing middlewares
    """
    def __init__(self, get_response=None):
        # disable the middleware if the tracer is not enabled
        # or if the auto instrumentation is disabled
        self.get_response = get_response
        if not settings.AUTO_INSTRUMENT:
            raise MiddlewareNotUsed


class TraceExceptionMiddleware(InstrumentationMixin):
    """
    Middleware that traces exceptions raised
    """
    def process_exception(self, request, exception):
        try:
            span = _get_req_span(request)
            if span:
                span.set_tag(http.STATUS_CODE, '500')
                span.set_traceback()  # will set the exception info
        except Exception:
            log.debug('error processing exception', exc_info=True)


class TraceMiddleware(InstrumentationMixin):
    """
    Middleware that traces Django requests
    """
    def process_request(self, request):
        tracer = settings.TRACER
        if settings.DISTRIBUTED_TRACING:
            propagator = HTTPPropagator()
            context = propagator.extract(request.META)
            # Only need to active the new context if something was propagated
            if context.trace_id:
                tracer.context_provider.activate(context)
        try:
            span = tracer.trace(
                'django.request',
                service=settings.DEFAULT_SERVICE,
                resource='unknown',  # will be filled by process view
                span_type=SpanTypes.WEB,
            )

            # set analytics sample rate
            # DEV: django is special case maintains separate configuration from config api
            if _analytics_enabled() and settings.ANALYTICS_SAMPLE_RATE is not None:
                span.set_tag(
                    ANALYTICS_SAMPLE_RATE_KEY,
                    settings.ANALYTICS_SAMPLE_RATE,
                )

            # Set HTTP Request tags
            span.set_tag(http.METHOD, request.method)
            span.set_tag(http.URL, get_request_uri(request))
            trace_query_string = settings.TRACE_QUERY_STRING
            if trace_query_string is None:
                trace_query_string = config.django.trace_query_string
            if trace_query_string:
                span.set_tag(http.QUERY_STRING, request.META['QUERY_STRING'])
            _set_req_span(request, span)
        except Exception:
            log.debug('error tracing request', exc_info=True)

    def process_view(self, request, view_func, *args, **kwargs):
        span = _get_req_span(request)
        if span:
            span.resource = func_name(view_func)

    def process_response(self, request, response):
        try:
            span = _get_req_span(request)
            if span:
                if response.status_code < 500 and span.error:
                    # remove any existing stack trace since it must have been
                    # handled appropriately
                    span._remove_exc_info()

                # If `process_view` was not called, try to determine the correct `span.resource` to set
                # DEV: `process_view` won't get called if a middle `process_request` returns an HttpResponse
                # DEV: `process_view` won't get called when internal error handlers are used (e.g. for 404 responses)
                if span.resource == 'unknown':
                    try:
                        # Attempt to lookup the view function from the url resolver
                        #   https://github.com/django/django/blob/38e2fdadfd9952e751deed662edf4c496d238f28/django/core/handlers/base.py#L104-L113  # noqa
                        urlconf = None
                        if hasattr(request, 'urlconf'):
                            urlconf = request.urlconf
                        resolver = get_resolver(urlconf)

                        # Try to resolve the Django view for handling this request
                        if getattr(request, 'request_match', None):
                            request_match = request.request_match
                        else:
                            # This may raise a `django.urls.exceptions.Resolver404` exception
                            request_match = resolver.resolve(request.path_info)
                        span.resource = func_name(request_match.func)
                    except Exception:
                        log.debug('error determining request view function', exc_info=True)

                        # If the view could not be found, try to set from a static list of
                        # known internal error handler views
                        span.resource = _django_default_views.get(response.status_code, 'unknown')

                span.set_tag(http.STATUS_CODE, response.status_code)
                span = _set_auth_tags(span, request)
                span.finish()
        except Exception:
            log.debug('error tracing request', exc_info=True)
        finally:
            return response


def _get_req_span(request):
    """ Return the datadog span from the given request. """
    return getattr(request, '_datadog_request_span', None)


def _set_req_span(request, span):
    """ Set the datadog span on the given request. """
    return setattr(request, '_datadog_request_span', span)


def _set_auth_tags(span, request):
    """ Patch any available auth tags from the request onto the span. """
    user = getattr(request, 'user', None)
    if not user:
        return span

    if hasattr(user, 'is_authenticated'):
        span.set_tag('django.user.is_authenticated', user_is_authenticated(user))

    uid = getattr(user, 'pk', None)
    if uid:
        span.set_tag('django.user.id', uid)

    uname = getattr(user, 'username', None)
    if uname:
        span.set_tag('django.user.name', uname)

    return span