elasticapm/contrib/django/middleware/__init__.py (145 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 logging
import threading
from types import FunctionType
from typing import Optional
import wrapt
from django.apps import apps
from django.conf import settings as django_settings
from django.http import HttpRequest, HttpResponse
import elasticapm
from elasticapm.conf import constants
from elasticapm.contrib.django.client import client, get_client
from elasticapm.utils import build_name_with_http_method_prefix, get_name_from_func
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
# no-op class for Django < 1.10
class MiddlewareMixin(object):
pass
def _is_ignorable_404(uri):
"""
Returns True if the given request *shouldn't* notify the site managers.
"""
urls = getattr(django_settings, "IGNORABLE_404_URLS", ())
return any(pattern.search(uri) for pattern in urls)
class ElasticAPMClientMiddlewareMixin(object):
@property
def client(self):
try:
app = apps.get_app_config("elasticapm")
return app.client
except LookupError:
return get_client()
class Catch404Middleware(MiddlewareMixin, ElasticAPMClientMiddlewareMixin):
def process_response(self, request, response):
if response.status_code != 404 or _is_ignorable_404(request.get_full_path()):
return response
if django_settings.DEBUG and not self.client.config.debug:
return response
data = {"level": logging.INFO, "logger": "http404"}
result = self.client.capture(
"Message",
request=request,
param_message={"message": "Page Not Found: %s", "params": [request.build_absolute_uri()]},
logger_name="http404",
level=logging.INFO,
)
request._elasticapm = {"service_name": data.get("service_name", self.client.config.service_name), "id": result}
return response
def get_name_from_middleware(wrapped, instance):
name = [type(instance).__name__, wrapped.__name__]
if type(instance).__module__:
name = [type(instance).__module__] + name
return ".".join(name)
def process_request_wrapper(wrapped, instance, args, kwargs):
response = wrapped(*args, **kwargs)
try:
if response is not None:
request = args[0]
elasticapm.set_transaction_name(
build_name_with_http_method_prefix(get_name_from_middleware(wrapped, instance), request)
)
finally:
return response
def process_response_wrapper(wrapped, instance, args, kwargs):
response = wrapped(*args, **kwargs)
try:
request, original_response = args
# if we haven't set the name in a view, and this middleware created
# a new response object, it's logged as the responsible transaction
# name
if not getattr(request, "_elasticapm_name_set", False) and response is not original_response:
elasticapm.set_transaction_name(
build_name_with_http_method_prefix(get_name_from_middleware(wrapped, instance), request)
)
finally:
return response
class TracingMiddleware(MiddlewareMixin, ElasticAPMClientMiddlewareMixin):
_elasticapm_instrumented = False
_instrumenting_lock = threading.Lock()
def __init__(self, *args, **kwargs) -> None:
super(TracingMiddleware, self).__init__(*args, **kwargs)
if not self._elasticapm_instrumented:
with self._instrumenting_lock:
if not self._elasticapm_instrumented:
if self.client.config.instrument_django_middleware:
self.instrument_middlewares()
TracingMiddleware._elasticapm_instrumented = True
def instrument_middlewares(self) -> None:
middlewares = getattr(django_settings, "MIDDLEWARE", None) or getattr(
django_settings, "MIDDLEWARE_CLASSES", None
)
if middlewares:
for middleware_path in middlewares:
module_path, class_name = middleware_path.rsplit(".", 1)
try:
module = import_module(module_path)
middleware_class = getattr(module, class_name)
if middleware_class == type(self):
# don't instrument ourselves
continue
if hasattr(middleware_class, "process_request"):
wrapt.wrap_function_wrapper(middleware_class, "process_request", process_request_wrapper)
if hasattr(middleware_class, "process_response"):
wrapt.wrap_function_wrapper(middleware_class, "process_response", process_response_wrapper)
except ImportError:
client.logger.warning("Can't instrument middleware %s", middleware_path)
def process_view(self, request: HttpRequest, view_func: FunctionType, view_args: list, view_kwargs: dict) -> None:
elasticapm.set_transaction_name(self.get_transaction_name(request, view_func), override=False)
request._elasticapm_name_set = True
def process_response(self, request: HttpRequest, response: HttpResponse):
if django_settings.DEBUG and not self.client.config.debug:
return response
try:
if hasattr(response, "status_code"):
if not getattr(request, "_elasticapm_name_set", False):
elasticapm.set_transaction_name(self.get_transaction_name(request), override=False)
elasticapm.set_context(
lambda: self.client.get_data_from_request(request, constants.TRANSACTION), "request"
)
elasticapm.set_context(
lambda: self.client.get_data_from_response(response, constants.TRANSACTION), "response"
)
elasticapm.set_context(lambda: self.client.get_user_info(request), "user")
elasticapm.set_transaction_result("HTTP {}xx".format(response.status_code // 100), override=False)
elasticapm.set_transaction_outcome(http_status_code=response.status_code, override=False)
except Exception:
self.client.error_logger.error("Exception during timing of request", exc_info=True)
return response
def get_transaction_name(self, request: HttpRequest, view_func: Optional[FunctionType] = None) -> str:
transaction_name = ""
if self.client.config.django_transaction_name_from_route and hasattr(request.resolver_match, "route"):
r = request.resolver_match
# if no route is defined (e.g. for the root URL), fall back on url_name and then function name
transaction_name = r.route or r.url_name or get_name_from_func(r.func)
elif view_func:
transaction_name = get_name_from_func(view_func)
if transaction_name:
transaction_name = build_name_with_http_method_prefix(transaction_name, request)
return transaction_name
class ErrorIdMiddleware(MiddlewareMixin):
"""
Appends the X-ElasticAPM-ErrorId response header for referencing a message within
the ElasticAPM datastore.
"""
def process_response(self, request, response):
if not getattr(request, "_elasticapm", None):
return response
response["X-ElasticAPM-ErrorId"] = request._elasticapm["id"]
return response
class LogMiddleware(MiddlewareMixin):
# Create a thread local variable to store the session in for logging
thread = threading.local()
def process_request(self, request) -> None:
self.thread.request = request