elasticapm/contrib/django/apps.py (122 lines of code) (raw):

# BSD 3-Clause License # # 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 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from functools import partial from django.apps import AppConfig from django.conf import settings as django_settings from elasticapm.conf import constants from elasticapm.contrib.django.client import get_client from elasticapm.utils.disttracing import TraceParent from elasticapm.utils.logging import get_logger from elasticapm.utils.wsgi import get_current_url logger = get_logger("elasticapm.traces") ERROR_DISPATCH_UID = "elasticapm-exceptions" REQUEST_START_DISPATCH_UID = "elasticapm-request-start" REQUEST_FINISH_DISPATCH_UID = "elasticapm-request-stop" MIDDLEWARE_NAME = "elasticapm.contrib.django.middleware.TracingMiddleware" TRACEPARENT_HEADER_NAME_WSGI = "HTTP_" + constants.TRACEPARENT_HEADER_NAME.upper().replace("-", "_") TRACEPARENT_LEGACY_HEADER_NAME_WSGI = "HTTP_" + constants.TRACEPARENT_LEGACY_HEADER_NAME.upper().replace("-", "_") TRACESTATE_HEADER_NAME_WSGI = "HTTP_" + constants.TRACESTATE_HEADER_NAME.upper().replace("-", "_") class ElasticAPMConfig(AppConfig): name = "elasticapm.contrib.django" label = "elasticapm" verbose_name = "ElasticAPM" def __init__(self, *args, **kwargs) -> None: super(ElasticAPMConfig, self).__init__(*args, **kwargs) self.client = None def ready(self) -> None: self.client = get_client() if self.client.config.autoinsert_django_middleware: self.insert_middleware(django_settings) register_handlers(self.client) if self.client.config.instrument and self.client.config.enabled: instrument(self.client) else: self.client.logger.debug("Skipping instrumentation. INSTRUMENT is set to False.") @staticmethod def insert_middleware(settings) -> None: if hasattr(settings, "MIDDLEWARE"): middleware_list = settings.MIDDLEWARE middleware_attr = "MIDDLEWARE" elif hasattr(settings, "MIDDLEWARE_CLASSES"): # can be removed when we drop support for Django 1.x middleware_list = settings.MIDDLEWARE_CLASSES middleware_attr = "MIDDLEWARE_CLASSES" else: logger.debug("Could not find middleware setting, not autoinserting tracing middleware") return is_tuple = isinstance(middleware_list, tuple) if is_tuple: middleware_list = list(middleware_list) elif not isinstance(middleware_list, list): logger.debug("%s setting is not of type list or tuple, not autoinserting tracing middleware") return if middleware_list is not None and MIDDLEWARE_NAME not in middleware_list: logger.debug("Inserting tracing middleware into settings.%s", middleware_attr) middleware_list.insert(0, MIDDLEWARE_NAME) if is_tuple: middleware_list = tuple(middleware_list) if middleware_list: setattr(settings, middleware_attr, middleware_list) def register_handlers(client) -> None: from django.core.signals import got_request_exception, request_finished, request_started from elasticapm.contrib.django.handlers import exception_handler # Connect to Django's internal signal handlers got_request_exception.disconnect(dispatch_uid=ERROR_DISPATCH_UID) got_request_exception.connect(partial(exception_handler, client), dispatch_uid=ERROR_DISPATCH_UID, weak=False) request_started.disconnect(dispatch_uid=REQUEST_START_DISPATCH_UID) request_started.connect( partial(_request_started_handler, client), dispatch_uid=REQUEST_START_DISPATCH_UID, weak=False ) request_finished.disconnect(dispatch_uid=REQUEST_FINISH_DISPATCH_UID) request_finished.connect( lambda sender, **kwargs: client.end_transaction() if _should_start_transaction(client) else None, dispatch_uid=REQUEST_FINISH_DISPATCH_UID, weak=False, ) # If we can import celery, register ourselves as exception handler try: import celery # noqa F401 from elasticapm.contrib.celery import register_exception_tracking try: register_exception_tracking(client) except Exception as e: client.logger.exception("Failed installing django-celery hook: %s" % e) except ImportError: client.logger.debug("Not instrumenting Celery, couldn't import") def _request_started_handler(client, sender, *args, **kwargs) -> None: if not _should_start_transaction(client): return # try to find trace id trace_parent = None if "environ" in kwargs: url = get_current_url(kwargs["environ"], strip_querystring=True, path_only=True) if client.should_ignore_url(url): logger.debug("Ignoring request due to %s matching transaction_ignore_urls") return trace_parent = TraceParent.from_headers( kwargs["environ"], TRACEPARENT_HEADER_NAME_WSGI, TRACEPARENT_LEGACY_HEADER_NAME_WSGI, TRACESTATE_HEADER_NAME_WSGI, ) elif "scope" in kwargs: scope = kwargs["scope"] fake_environ = {"SCRIPT_NAME": scope.get("root_path", ""), "PATH_INFO": scope["path"], "QUERY_STRING": ""} url = get_current_url(fake_environ, strip_querystring=True, path_only=True) if client.should_ignore_url(url): logger.debug("Ignoring request due to %s matching transaction_ignore_urls") return if "headers" in scope: trace_parent = TraceParent.from_headers(scope["headers"]) client.begin_transaction("request", trace_parent=trace_parent) def instrument(client) -> None: """ Auto-instruments code to get nice spans """ from elasticapm.instrumentation.control import instrument instrument() try: import celery # noqa F401 from elasticapm.contrib.celery import register_instrumentation register_instrumentation(client) except ImportError: client.logger.debug("Not instrumenting Celery, couldn't import") def _should_start_transaction(client): middleware_attr = "MIDDLEWARE" if getattr(django_settings, "MIDDLEWARE", None) is not None else "MIDDLEWARE_CLASSES" middleware = getattr(django_settings, middleware_attr) return ( (not django_settings.DEBUG or client.config.debug) and middleware and "elasticapm.contrib.django.middleware.TracingMiddleware" in middleware )