elasticapm/contrib/django/client.py (202 lines of code) (raw):

# BSD 3-Clause License # # Copyright (c) 2012, the Sentry Team, see AUTHORS for more details # Copyright (c) 2019, Elasticsearch BV # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # * Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE from __future__ import absolute_import import django from django.conf import settings as django_settings from django.db import DatabaseError from django.http import HttpRequest try: from rest_framework.request import Request as DrfRequest except ImportError: DrfRequest = HttpRequest from elasticapm import get_client as _get_client from elasticapm.base import Client from elasticapm.conf import constants from elasticapm.contrib.django.utils import get_raw_uri, iterate_with_template_sources from elasticapm.utils import compat, encoding, get_url_dict from elasticapm.utils.encoding import long_field from elasticapm.utils.logging import get_logger from elasticapm.utils.module_import import import_string from elasticapm.utils.wsgi import get_environ, get_headers __all__ = ("DjangoClient",) default_client_class = "elasticapm.contrib.django.DjangoClient" def get_client(): """ Get an ElasticAPM client. :param client: :return: :rtype: elasticapm.base.Client """ c = _get_client() if c: return c config = getattr(django_settings, "ELASTIC_APM", {}) client = config.get("CLIENT", default_client_class) client_class = import_string(client) instance = client_class() # `instance` will already be in elasticapm.base.CLIENT_SINGLETON due to the # `__init__()` for Client return instance class DjangoClient(Client): logger = get_logger("elasticapm.errors.client.django") def __init__(self, config=None, **inline) -> None: if config is None: config = getattr(django_settings, "ELASTIC_APM", {}) if "framework_name" not in inline: inline["framework_name"] = "django" inline["framework_version"] = django.get_version() super(DjangoClient, self).__init__(config, **inline) def get_user_info(self, request): user_info = {} if not hasattr(request, "user"): return user_info try: user = request.user if hasattr(user, "is_authenticated"): if callable(user.is_authenticated): user_info["is_authenticated"] = user.is_authenticated() else: user_info["is_authenticated"] = bool(user.is_authenticated) if hasattr(user, "id"): user_info["id"] = encoding.keyword_field(user.id) if hasattr(user, "get_username"): user_info["username"] = encoding.keyword_field(encoding.force_text(user.get_username())) elif hasattr(user, "username"): user_info["username"] = encoding.keyword_field(encoding.force_text(user.username)) if hasattr(user, "email"): user_info["email"] = encoding.force_text(user.email) except DatabaseError: # If the connection is closed or similar, we'll just skip this return {} return user_info def get_data_from_request(self, request, event_type): result = { "env": dict(get_environ(request.META)), "method": request.method, "socket": {"remote_address": request.META.get("REMOTE_ADDR")}, "cookies": dict(request.COOKIES), } if self.config.capture_headers: request_headers = dict(get_headers(request.META)) for key, value in request_headers.items(): if isinstance(value, (int, float)): request_headers[key] = str(value) result["headers"] = request_headers if request.method in constants.HTTP_WITH_BODY: capture_body = self.config.capture_body in ("all", event_type) if not capture_body: result["body"] = "[REDACTED]" else: content_type = request.META.get("CONTENT_TYPE") if content_type == "application/x-www-form-urlencoded": data = compat.multidict_to_dict(request.POST) elif content_type and content_type.startswith("multipart/form-data"): data = compat.multidict_to_dict(request.POST) if request.FILES: data["_files"] = {field: file.name for field, file in request.FILES.items()} else: try: data = request.body except Exception as e: self.logger.debug("Can't capture request body: %s", str(e)) data = "<unavailable>" if data is not None: # Can we apply this as a processor instead? # https://github.com/elastic/apm-agent-python/issues/305 result["body"] = long_field(data) url = get_raw_uri(request) try: result["url"] = get_url_dict(url) except ValueError as exc: self.logger.warning(f"URL parsing failed: {exc}") return result def get_data_from_response(self, response, event_type): result = {"status_code": response.status_code} if self.config.capture_headers and hasattr(response, "items"): response_headers = dict(response.items()) for key, value in response_headers.items(): if isinstance(value, (int, float)): response_headers[key] = str(value) result["headers"] = response_headers return result def capture(self, event_type, request=None, **kwargs): if "context" not in kwargs: kwargs["context"] = context = {} else: context = kwargs["context"] is_http_request = isinstance(request, (HttpRequest, DrfRequest)) if is_http_request: context["request"] = self.get_data_from_request(request, constants.ERROR) context["user"] = self.get_user_info(request) result = super(DjangoClient, self).capture(event_type, **kwargs) if is_http_request: # attach the elasticapm object to the request request._elasticapm = {"service_name": self.config.service_name, "id": result} return result def _get_stack_info_for_trace( self, frames, library_frame_context_lines=None, in_app_frame_context_lines=None, with_locals=True, locals_processor_func=None, ): """If the stacktrace originates within the elasticapm module, it will skip frames until some other module comes up.""" return list( iterate_with_template_sources( frames, with_locals=with_locals, library_frame_context_lines=library_frame_context_lines, in_app_frame_context_lines=in_app_frame_context_lines, include_paths_re=self.include_paths_re, exclude_paths_re=self.exclude_paths_re, locals_processor_func=locals_processor_func, ) ) def send(self, url, **kwargs): """ Serializes and signs ``data`` and passes the payload off to ``send_remote`` If ``server`` was passed into the constructor, this will serialize the data and pipe it to the server using ``send_remote()``. """ if self.config.server_url: return super(DjangoClient, self).send(url, **kwargs) else: self.error_logger.error("No server configured, and elasticapm not installed. Cannot send message") return None class ProxyClient(object): """ A proxy which represents the current client at all times. """ # introspection support: __members__ = property(lambda x: x.__dir__()) # Need to pretend to be the wrapped class, for the sake of objects that care # about this (especially in equality tests) __class__ = property(lambda x: get_client().__class__) __dict__ = property(lambda o: get_client().__dict__) __repr__ = lambda: repr(get_client()) __getattr__ = lambda x, o: getattr(get_client(), o) __setattr__ = lambda x, o, v: setattr(get_client(), o, v) __delattr__ = lambda x, o: delattr(get_client(), o) __lt__ = lambda x, o: get_client() < o __le__ = lambda x, o: get_client() <= o __eq__ = lambda x, o: get_client() == o __ne__ = lambda x, o: get_client() != o __gt__ = lambda x, o: get_client() > o __ge__ = lambda x, o: get_client() >= o __hash__ = lambda x: hash(get_client()) # attributes are currently not callable # __call__ = lambda x, *a, **kw: get_client()(*a, **kw) __nonzero__ = lambda x: bool(get_client()) __len__ = lambda x: len(get_client()) __getitem__ = lambda x, i: get_client()[i] __iter__ = lambda x: iter(get_client()) __contains__ = lambda x, i: i in get_client() __getslice__ = lambda x, i, j: get_client()[i:j] __add__ = lambda x, o: get_client() + o __sub__ = lambda x, o: get_client() - o __mul__ = lambda x, o: get_client() * o __floordiv__ = lambda x, o: get_client() // o __mod__ = lambda x, o: get_client() % o __divmod__ = lambda x, o: get_client().__divmod__(o) __pow__ = lambda x, o: get_client() ** o __lshift__ = lambda x, o: get_client() << o __rshift__ = lambda x, o: get_client() >> o __and__ = lambda x, o: get_client() & o __xor__ = lambda x, o: get_client() ^ o __or__ = lambda x, o: get_client() | o __div__ = lambda x, o: get_client().__div__(o) __truediv__ = lambda x, o: get_client().__truediv__(o) __neg__ = lambda x: -(get_client()) __pos__ = lambda x: +(get_client()) __abs__ = lambda x: abs(get_client()) __invert__ = lambda x: ~(get_client()) __complex__ = lambda x: complex(get_client()) __int__ = lambda x: int(get_client()) __float__ = lambda x: float(get_client()) __str__ = lambda x: str(get_client()) __unicode__ = lambda x: str(get_client()) __oct__ = lambda x: oct(get_client()) __hex__ = lambda x: hex(get_client()) __index__ = lambda x: get_client().__index__() __coerce__ = lambda x, o: x.__coerce__(x, o) __enter__ = lambda x: x.__enter__() __exit__ = lambda x, *a, **kw: x.__exit__(*a, **kw) client = ProxyClient() def _get_installed_apps_paths(): """ Generate a list of modules in settings.INSTALLED_APPS. """ out = set() for app in django_settings.INSTALLED_APPS: out.add(app) return out