def _instrument_view(django, view): """Helper to wrap Django views.""" # All views should be callable, double check before doing anything if not callable(view): return view # Patch view HTTP methods and lifecycle methods http_method_names = getattr(view, "http_method_names", ("get", "delete", "post", "options", "head")) lifecycle_methods = ("setup", "dispatch", "http_method_not_allowed") for name in list(http_method_names) + list(lifecycle_methods): try: func = getattr(view, name, None) if not func or isinstance(func, wrapt.ObjectProxy): continue resource = "{0}.{1}".format(func_name(view), name) op_name = "django.view.{0}".format(name) trace_utils.wrap( view, name, traced_func(django, name=op_name, resource=resource)) except Exception: log.debug("Failed to instrument Django view %r function %s", view, name, exc_info=True) # Patch response methods response_cls = getattr(view, "response_class", None) if response_cls: methods = ("render", ) for name in methods: try: func = getattr(response_cls, name, None) # Do not wrap if the method does not exist or is already wrapped if not func or isinstance(func, wrapt.ObjectProxy): continue resource = "{0}.{1}".format(func_name(response_cls), name) op_name = "django.response.{0}".format(name) trace_utils.wrap( response_cls, name, traced_func(django, name=op_name, resource=resource)) except Exception: log.debug( "Failed to instrument Django response %r function %s", response_cls, name, exc_info=True) # If the view itself is not wrapped, wrap it if not isinstance(view, wrapt.ObjectProxy): view = wrapt.FunctionWrapper( view, traced_func(django, "django.view", resource=func_name(view), ignored_excs=[django.http.Http404])) return view
def instrument_view(django, view): """Helper to wrap Django views.""" # All views should be callable, double check before doing anything if not callable(view) or isinstance(view, wrapt.ObjectProxy): return view # Patch view HTTP methods and lifecycle methods http_method_names = getattr(view, "http_method_names", ("get", "delete", "post", "options", "head")) lifecycle_methods = ("setup", "dispatch", "http_method_not_allowed") for name in list(http_method_names) + list(lifecycle_methods): try: # View methods can be staticmethods func = getattr_static(view, name, None) if not func or isinstance(func, wrapt.ObjectProxy): continue resource = "{0}.{1}".format(func_name(view), name) op_name = "django.view.{0}".format(name) # Set attribute here rather than using wrapt.wrappers.wrap_function_wrapper # since it will not resolve attribute to staticmethods wrapper = wrapt.FunctionWrapper( func, traced_func(django, name=op_name, resource=resource)) setattr(view, name, wrapper) except Exception: log.debug("Failed to instrument Django view %r function %s", view, name, exc_info=True) # Patch response methods response_cls = getattr(view, "response_class", None) if response_cls: methods = ("render", ) for name in methods: try: func = getattr(response_cls, name, None) # Do not wrap if the method does not exist or is already wrapped if not func or isinstance(func, wrapt.ObjectProxy): continue resource = "{0}.{1}".format(func_name(response_cls), name) op_name = "django.response.{0}".format(name) wrap(response_cls, name, traced_func(django, name=op_name, resource=resource)) except Exception: log.debug( "Failed to instrument Django response %r function %s", response_cls, name, exc_info=True) # Return a wrapped version of this view return wrapt.FunctionWrapper( view, traced_func(django, "django.view", resource=func_name(view)))
def _set_request_tags(django, span, request): span.set_tag("django.request.class", func_name(request)) span.set_tag(http.METHOD, request.method) if django.VERSION >= (2, 2, 0): headers = request.headers else: headers = {} for header, value in request.META.items(): name = from_wsgi_header(header) if name: headers[name] = value store_request_headers(headers, span, config.django) user = getattr(request, "user", None) if user is not None: 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) if config.django.include_user_name: username = getattr(user, "username", None) if username: span.set_tag("django.user.name", username)
def traced_template_render(django, pin, wrapped, instance, args, kwargs): """Instrument django.template.base.Template.render for tracing template rendering.""" template_name = getattr(instance, "name", None) if template_name: resource = template_name else: resource = "{0}.{1}".format(func_name(instance), wrapped.__name__) with pin.tracer.trace("django.template.render", resource=resource, span_type=http.TEMPLATE) as span: if template_name: span.set_tag("django.template.name", template_name) engine = getattr(instance, "engine", None) if engine: span.set_tag("django.template.engine.class", func_name(engine)) return wrapped(*args, **kwargs)
def _set_request_tags(django, span, request): span._set_str_tag("django.request.class", func_name(request)) user = getattr(request, "user", None) if user is not None: # Note: getattr calls to user / user_is_authenticated may result in ImproperlyConfigured exceptions from # Django's get_user_model(): # https://github.com/django/django/blob/a464ead29db8bf6a27a5291cad9eb3f0f3f0472b/django/contrib/auth/__init__.py try: if hasattr(user, "is_authenticated"): span._set_str_tag("django.user.is_authenticated", user_is_authenticated(user)) uid = getattr(user, "pk", None) if uid: span._set_str_tag("django.user.id", str(uid)) if config.django.include_user_name: username = getattr(user, "username", None) if username: span._set_str_tag("django.user.name", username) except Exception: log.debug( "Error retrieving authentication information for user %r", user, exc_info=True)
def traced_as_view(django, pin, func, instance, args, kwargs): """ Wrapper for django's View.as_view class method """ try: instrument_view(django, instance) except Exception: log.debug("Failed to instrument Django view %r", instance, exc_info=True) view = func(*args, **kwargs) return wrapt.FunctionWrapper(view, traced_func(django, "django.view", resource=func_name(view)))
def _set_resolver_tags(pin, span, request): # Default to just the HTTP method when we cannot determine a reasonable resource resource = request.method try: # Get resolver match result and build resource name pieces resolver_match = request.resolver_match if not resolver_match: # The request quite likely failed (e.g. 404) so we do the resolution anyway. resolver = get_resolver(getattr(request, "urlconf", None)) resolver_match = resolver.resolve(request.path_info) handler = func_name(resolver_match[0]) if config.django.use_handler_resource_format: resource = " ".join((request.method, handler)) elif config.django.use_legacy_resource_format: resource = handler else: # In Django >= 2.2.0 we can access the original route or regex pattern # TODO: Validate if `resolver.pattern.regex.pattern` is available on django<2.2 if DJANGO22: # Determine the resolver and resource name for this request route = get_django_2_route(request, resolver_match) if route: resource = " ".join((request.method, route)) span._set_str_tag("http.route", route) else: resource = " ".join((request.method, handler)) span._set_str_tag("django.view", resolver_match.view_name) set_tag_array(span, "django.namespace", resolver_match.namespaces) # Django >= 2.0.0 if hasattr(resolver_match, "app_names"): set_tag_array(span, "django.app", resolver_match.app_names) except Resolver404: # Normalize all 404 requests into a single resource name # DEV: This is for potential cardinality issues resource = " ".join((request.method, "404")) except Exception: log.debug( "Failed to resolve request path %r with path info %r", request, getattr(request, "path_info", "not-set"), exc_info=True, ) finally: # Only update the resource name if it was not explicitly set # by anyone during the request lifetime if span.resource == REQUEST_DEFAULT_RESOURCE: span.resource = resource
def _before_request_tags(pin, span, request): # DEV: Do not set `span.resource` here, leave it as `None` # until `_set_resolver_tags` so we can know if the user # has explicitly set it during the request lifetime span.service = trace_utils.int_service(pin, config.django) span.span_type = SpanTypes.WEB span.metrics[SPAN_MEASURED_KEY] = 1 analytics_sr = config.django.get_analytics_sample_rate( use_global_config=True) if analytics_sr is not None: span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, analytics_sr) span._set_str_tag("django.request.class", func_name(request))
def traced_cache(django, pin, func, instance, args, kwargs): if not config.django.instrument_caches: return func(*args, **kwargs) # get the original function method with pin.tracer.trace("django.cache", span_type=SpanTypes.CACHE, service=config.django.cache_service_name) as span: # update the resource name and tag the cache backend span.resource = utils.resource_from_cache_prefix(func_name(func), instance) cache_backend = "{}.{}".format(instance.__module__, instance.__class__.__name__) span.set_tag("django.cache.backend", cache_backend) if args: keys = utils.quantize_key_values(args[0]) span.set_tag("django.cache.key", keys) return func(*args, **kwargs)
def _set_request_tags(django, span, request): span.set_tag("django.request.class", func_name(request)) user = getattr(request, "user", None) if user is not None: 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) if config.django.include_user_name: username = getattr(user, "username", None) if username: span.set_tag("django.user.name", username)
def _set_request_tags(span, request): span.set_tag("django.request.class", func_name(request)) span.set_tag(http.METHOD, request.method) user = getattr(request, "user", None) if user is not None: 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) username = getattr(user, "username", None) if username: span.set_tag("django.user.name", username)
def traced_get_response(django, pin, func, instance, args, kwargs): """Trace django.core.handlers.base.BaseHandler.get_response() (or other implementations). This is the main entry point for requests. Django requests are handled by a Handler.get_response method (inherited from base.BaseHandler). This method invokes the middleware chain and returns the response generated by the chain. """ request = kwargs.get("request", args[0]) if request is None: return func(*args, **kwargs) try: request_headers = request.META if config.django.distributed_tracing_enabled: context = propagator.extract(request_headers) if context.trace_id: pin.tracer.context_provider.activate(context) # Determine the resolver and resource name for this request resolver = get_resolver(getattr(request, "urlconf", None)) if django.VERSION < (1, 10, 0): error_type_404 = django.core.urlresolvers.Resolver404 else: error_type_404 = django.urls.exceptions.Resolver404 route = None resolver_match = None resource = request.method try: # Resolve the requested url and build resource name pieces resolver_match = resolver.resolve(request.path_info) handler, _, _ = resolver_match handler = func_name(handler) urlpattern = "" resource_format = None if config.django.use_handler_resource_format: resource_format = "{method} {handler}" elif config.django.use_legacy_resource_format: resource_format = "{handler}" else: # In Django >= 2.2.0 we can access the original route or regex pattern # TODO: Validate if `resolver.pattern.regex.pattern` is available on django<2.2 if django.VERSION >= (2, 2, 0): route = utils.get_django_2_route(resolver, resolver_match) resource_format = "{method} {urlpattern}" else: resource_format = "{method} {handler}" if route is not None: urlpattern = route resource = resource_format.format(method=request.method, urlpattern=urlpattern, handler=handler) except error_type_404: # Normalize all 404 requests into a single resource name # DEV: This is for potential cardinality issues resource = "{0} 404".format(request.method) except Exception: log.debug( "Failed to resolve request path %r with path info %r", request, getattr(request, "path_info", "not-set"), exc_info=True, ) except Exception: log.debug("Failed to trace django request %r", args, exc_info=True) return func(*args, **kwargs) else: with pin.tracer.trace( "django.request", resource=resource, service=trace_utils.int_service(pin, config.django), span_type=SpanTypes.WEB, ) as span: analytics_sr = config.django.get_analytics_sample_rate( use_global_config=True) if analytics_sr is not None: span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, analytics_sr) # Not a 404 request if resolver_match: span.set_tag("django.view", resolver_match.view_name) utils.set_tag_array(span, "django.namespace", resolver_match.namespaces) # Django >= 2.0.0 if hasattr(resolver_match, "app_names"): utils.set_tag_array(span, "django.app", resolver_match.app_names) if route: span.set_tag("http.route", route) # Set HTTP Request tags response = func(*args, **kwargs) # Note: this call must be done after the function call because # some attributes (like `user`) are added to the request through # the middleware chain _set_request_tags(django, span, request) if response: status = response.status_code span.set_tag("django.response.class", func_name(response)) if hasattr(response, "template_name"): # template_name is a bit of a misnomer, as it could be any of: # a list of strings, a tuple of strings, a single string, or an instance of Template # for more detail, see: # https://docs.djangoproject.com/en/3.0/ref/template-response/#django.template.response.SimpleTemplateResponse.template_name template = response.template_name if isinstance(template, six.string_types): template_names = [template] elif isinstance( template, ( list, tuple, ), ): template_names = template elif hasattr(template, "template"): # ^ checking by attribute here because # django backend implementations don't have a common base # `.template` is also the most consistent across django versions template_names = [template.template.name] else: template_names = None utils.set_tag_array(span, "django.response.template", template_names) url = utils.get_request_uri(request) if django.VERSION >= (2, 2, 0): request_headers = request.headers else: request_headers = {} for header, value in request.META.items(): name = from_wsgi_header(header) if name: request_headers[name] = value response_headers = dict(response.items()) trace_utils.set_http_meta( span, config.django, method=request.method, url=url, status_code=status, query=request.META.get("QUERY_STRING", None), request_headers=request_headers, response_headers=response_headers, ) return response
def _after_request_tags(pin, span, request, response): # Response can be None in the event that the request failed # We still want to set additional request tags that are resolved # during the request. try: user = getattr(request, "user", None) if user is not None: # Note: getattr calls to user / user_is_authenticated may result in ImproperlyConfigured exceptions from # Django's get_user_model(): # https://github.com/django/django/blob/a464ead29db8bf6a27a5291cad9eb3f0f3f0472b/django/contrib/auth/__init__.py try: if hasattr(user, "is_authenticated"): span._set_str_tag("django.user.is_authenticated", str(user_is_authenticated(user))) uid = getattr(user, "pk", None) if uid: span._set_str_tag("django.user.id", str(uid)) if config.django.include_user_name: username = getattr(user, "username", None) if username: span._set_str_tag("django.user.name", username) except Exception: log.debug( "Error retrieving authentication information for user %r", user, exc_info=True) if response: status = response.status_code span._set_str_tag("django.response.class", func_name(response)) if hasattr(response, "template_name"): # template_name is a bit of a misnomer, as it could be any of: # a list of strings, a tuple of strings, a single string, or an instance of Template # for more detail, see: # https://docs.djangoproject.com/en/3.0/ref/template-response/#django.template.response.SimpleTemplateResponse.template_name template = response.template_name if isinstance(template, six.string_types): template_names = [template] elif isinstance( template, ( list, tuple, ), ): template_names = template elif hasattr(template, "template"): # ^ checking by attribute here because # django backend implementations don't have a common base # `.template` is also the most consistent across django versions template_names = [template.template.name] else: template_names = None set_tag_array(span, "django.response.template", template_names) url = get_request_uri(request) if DJANGO22: request_headers = request.headers else: request_headers = {} for header, value in request.META.items(): name = from_wsgi_header(header) if name: request_headers[name] = value # DEV: Resolve the view and resource name at the end of the request in case # urlconf changes at any point during the request _set_resolver_tags(pin, span, request) response_headers = dict(response.items()) if response else {} trace_utils.set_http_meta( span, config.django, method=request.method, url=url, status_code=status, query=request.META.get("QUERY_STRING", None), request_headers=request_headers, response_headers=response_headers, ) finally: if span.resource == REQUEST_DEFAULT_RESOURCE: span.resource = request.method
def traced_get_response(django, pin, func, instance, args, kwargs): """Trace django.core.handlers.base.BaseHandler.get_response() (or other implementations). This is the main entry point for requests. Django requests are handled by a Handler.get_response method (inherited from base.BaseHandler). This method invokes the middleware chain and returns the response generated by the chain. """ request = kwargs.get("request", args[0]) if request is None: return func(*args, **kwargs) try: request_headers = request.META if config.django.distributed_tracing_enabled: context = propagator.extract(request_headers) if context.trace_id: pin.tracer.context_provider.activate(context) # Determine the resolver and resource name for this request resolver = get_resolver(getattr(request, "urlconf", None)) if django.VERSION < (1, 10, 0): error_type_404 = django.core.urlresolvers.Resolver404 else: error_type_404 = django.urls.exceptions.Resolver404 route = None resolver_match = None resource = request.method try: # Resolve the requested url resolver_match = resolver.resolve(request.path_info) # Determine the resource name to use # In Django >= 2.2.0 we can access the original route or regex pattern if django.VERSION >= (2, 2, 0): route = utils.get_django_2_route(resolver, resolver_match) if route: resource = "{0} {1}".format(request.method, route) else: resource = request.method # Older versions just use the view/handler name, e.g. `views.MyView.handler` else: # TODO: Validate if `resolver.pattern.regex.pattern` is available or not callback, callback_args, callback_kwargs = resolver_match resource = "{0} {1}".format(request.method, func_name(callback)) except error_type_404: # Normalize all 404 requests into a single resource name # DEV: This is for potential cardinality issues resource = "{0} 404".format(request.method) except Exception: log.debug( "Failed to resolve request path %r with path info %r", request, getattr(request, "path_info", "not-set"), exc_info=True, ) except Exception: log.debug("Failed to trace django request %r", args, exc_info=True) return func(*args, **kwargs) else: with pin.tracer.trace("django.request", resource=resource, service=config.django["service_name"], span_type=SpanTypes.HTTP) as span: analytics_sr = config.django.get_analytics_sample_rate( use_global_config=True) if analytics_sr is not None: span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, analytics_sr) if config.django.http.trace_query_string: span.set_tag(http.QUERY_STRING, request_headers["QUERY_STRING"]) # Not a 404 request if resolver_match: span.set_tag("django.view", resolver_match.view_name) utils.set_tag_array(span, "django.namespace", resolver_match.namespaces) # Django >= 2.0.0 if hasattr(resolver_match, "app_names"): utils.set_tag_array(span, "django.app", resolver_match.app_names) if route: span.set_tag("http.route", route) # Set HTTP Request tags # Build `http.url` tag value from request info # DEV: We are explicitly omitting query strings since they may contain sensitive information span.set_tag( http.URL, parse.urlunparse( parse.ParseResult( scheme=request.scheme, netloc=request.get_host( ), # this will include `host:port` path=request.path, params="", query="", fragment="", )), ) response = func(*args, **kwargs) # Note: this call must be done after the function call because # some attributes (like `user`) are added to the request through # the middleware chain _set_request_tags(span, request) if response: span.set_tag(http.STATUS_CODE, response.status_code) if 500 <= response.status_code < 600: span.error = 1 span.set_tag("django.response.class", func_name(response)) if hasattr(response, "template_name"): utils.set_tag_array(span, "django.response.template", response.template_name) return response
def traced_get_response(django, pin, func, instance, args, kwargs): """Trace django.core.handlers.base.BaseHandler.get_response() (or other implementations). This is the main entry point for requests. Django requests are handled by a Handler.get_response method (inherited from base.BaseHandler). This method invokes the middleware chain and returns the response generated by the chain. """ request = kwargs.get("request", args[0]) if request is None: return func(*args, **kwargs) try: request_headers = request.META if config.django.distributed_tracing_enabled: context = propagator.extract(request_headers) if context.trace_id: pin.tracer.context_provider.activate(context) # Determine the resolver and resource name for this request resolver = get_resolver(getattr(request, "urlconf", None)) if django.VERSION < (1, 10, 0): error_type_404 = django.core.urlresolvers.Resolver404 else: error_type_404 = django.urls.exceptions.Resolver404 route = None resolver_match = None resource = request.method try: # Resolve the requested url resolver_match = resolver.resolve(request.path_info) # Determine the resource name to use # In Django >= 2.2.0 we can access the original route or regex pattern if django.VERSION >= (2, 2, 0): route = utils.get_django_2_route(resolver, resolver_match) if route: resource = "{0} {1}".format(request.method, route) else: resource = request.method # Older versions just use the view/handler name, e.g. `views.MyView.handler` else: # TODO: Validate if `resolver.pattern.regex.pattern` is available or not callback, callback_args, callback_kwargs = resolver_match resource = "{0} {1}".format(request.method, func_name(callback)) except error_type_404: # Normalize all 404 requests into a single resource name # DEV: This is for potential cardinality issues resource = "{0} 404".format(request.method) except Exception: log.debug( "Failed to resolve request path %r with path info %r", request, getattr(request, "path_info", "not-set"), exc_info=True, ) except Exception: log.debug("Failed to trace django request %r", args, exc_info=True) return func(*args, **kwargs) else: with pin.tracer.trace("django.request", resource=resource, service=config.django["service_name"], span_type=SpanTypes.HTTP) as span: analytics_sr = config.django.get_analytics_sample_rate( use_global_config=True) if analytics_sr is not None: span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, analytics_sr) if config.django.http.trace_query_string: span.set_tag(http.QUERY_STRING, request_headers["QUERY_STRING"]) # Not a 404 request if resolver_match: span.set_tag("django.view", resolver_match.view_name) utils.set_tag_array(span, "django.namespace", resolver_match.namespaces) # Django >= 2.0.0 if hasattr(resolver_match, "app_names"): utils.set_tag_array(span, "django.app", resolver_match.app_names) if route: span.set_tag("http.route", route) # Set HTTP Request tags span.set_tag(http.URL, utils.get_request_uri(request)) response = func(*args, **kwargs) # Note: this call must be done after the function call because # some attributes (like `user`) are added to the request through # the middleware chain _set_request_tags(span, request) if response: span.set_tag(http.STATUS_CODE, response.status_code) if 500 <= response.status_code < 600: span.error = 1 span.set_tag("django.response.class", func_name(response)) if hasattr(response, "template_name"): # template_name is a bit of a misnomer, as it could be any of: # a list of strings, a tuple of strings, a single string, or an instance of Template # for more detail, see: # https://docs.djangoproject.com/en/3.0/ref/template-response/#django.template.response.SimpleTemplateResponse.template_name template = response.template_name if isinstance(template, six.string_types): template_names = [template] elif isinstance(template, ( list, tuple, )): template_names = template elif hasattr(template, "template"): # ^ checking by attribute here because # django backend implementations don't have a common base # `.template` is also the most consistent across django versions template_names = [template.template.name] else: template_names = None utils.set_tag_array(span, "django.response.template", template_names) return response