mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-29 13:12:39 +08:00
Support request and resposne hooks for Django instrumentation (#407)
This commit is contained in:
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
- 'release/*'
|
- 'release/*'
|
||||||
pull_request:
|
pull_request:
|
||||||
env:
|
env:
|
||||||
CORE_REPO_SHA: 94bf80f3870bceefa72d9a61353eaf5d7dd30993
|
CORE_REPO_SHA: cad261e5dae1fe986c87e6965664b45cc9ab73c3
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
|
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
|
||||||
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
|
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
|
||||||
|
- `opentelemetry-instrumenation-django` now supports request and response hooks.
|
||||||
|
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
|
||||||
|
|
||||||
## [0.19b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.19b0) - 2021-03-26
|
## [0.19b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.19b0) - 2021-03-26
|
||||||
|
|
||||||
|
@ -45,6 +45,22 @@ will extract path_info and content_type attributes from every traced request and
|
|||||||
|
|
||||||
Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes
|
Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes
|
||||||
|
|
||||||
|
Request and Response hooks
|
||||||
|
***************************
|
||||||
|
The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request
|
||||||
|
and right before the span is finished while processing a response. The hooks can be configured as follows:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
def request_hook(span, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def response_hook(span, request, response):
|
||||||
|
pass
|
||||||
|
|
||||||
|
DjangoInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
|
||||||
|
|
||||||
|
|
||||||
References
|
References
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
@ -11,6 +11,67 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Instrument `django`_ to trace Django applications.
|
||||||
|
|
||||||
|
.. _django: https://pypi.org/project/django/
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||||
|
|
||||||
|
DjangoInstrumentor().instrument()
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Exclude lists
|
||||||
|
*************
|
||||||
|
To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude.
|
||||||
|
|
||||||
|
For example,
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
export OTEL_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck"
|
||||||
|
|
||||||
|
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
|
||||||
|
|
||||||
|
Request attributes
|
||||||
|
********************
|
||||||
|
To extract certain attributes from Django's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma
|
||||||
|
delimited list of request attribute names.
|
||||||
|
|
||||||
|
For example,
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type'
|
||||||
|
|
||||||
|
will extract path_info and content_type attributes from every traced request and add them as span attritbues.
|
||||||
|
|
||||||
|
Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes
|
||||||
|
|
||||||
|
Request and Response hooks
|
||||||
|
***************************
|
||||||
|
The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request
|
||||||
|
and right before the span is finished while processing a response. The hooks can be configured as follows:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
def request_hook(span, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def response_hook(span, request, response):
|
||||||
|
pass
|
||||||
|
|
||||||
|
DjangoInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
|
||||||
|
"""
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os import environ
|
from os import environ
|
||||||
@ -44,6 +105,11 @@ class DjangoInstrumentor(BaseInstrumentor):
|
|||||||
if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False":
|
if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_DjangoMiddleware._otel_request_hook = kwargs.pop("request_hook", None)
|
||||||
|
_DjangoMiddleware._otel_response_hook = kwargs.pop(
|
||||||
|
"response_hook", None
|
||||||
|
)
|
||||||
|
|
||||||
# This can not be solved, but is an inherent problem of this approach:
|
# This can not be solved, but is an inherent problem of this approach:
|
||||||
# the order of middleware entries matters, and here you have no control
|
# the order of middleware entries matters, and here you have no control
|
||||||
# on that:
|
# on that:
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from time import time
|
from time import time
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
from opentelemetry.context import attach, detach
|
from opentelemetry.context import attach, detach
|
||||||
from opentelemetry.instrumentation.django.version import __version__
|
from opentelemetry.instrumentation.django.version import __version__
|
||||||
@ -24,7 +27,7 @@ from opentelemetry.instrumentation.wsgi import (
|
|||||||
wsgi_getter,
|
wsgi_getter,
|
||||||
)
|
)
|
||||||
from opentelemetry.propagate import extract
|
from opentelemetry.propagate import extract
|
||||||
from opentelemetry.trace import SpanKind, get_tracer, use_span
|
from opentelemetry.trace import Span, SpanKind, get_tracer, use_span
|
||||||
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
|
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -62,6 +65,11 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
_traced_request_attrs = get_traced_request_attrs("DJANGO")
|
_traced_request_attrs = get_traced_request_attrs("DJANGO")
|
||||||
_excluded_urls = get_excluded_urls("DJANGO")
|
_excluded_urls = get_excluded_urls("DJANGO")
|
||||||
|
|
||||||
|
_otel_request_hook: Callable[[Span, HttpRequest], None] = None
|
||||||
|
_otel_response_hook: Callable[
|
||||||
|
[Span, HttpRequest, HttpResponse], None
|
||||||
|
] = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_span_name(request):
|
def _get_span_name(request):
|
||||||
try:
|
try:
|
||||||
@ -125,6 +133,11 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
request.META[self._environ_span_key] = span
|
request.META[self._environ_span_key] = span
|
||||||
request.META[self._environ_token] = token
|
request.META[self._environ_token] = token
|
||||||
|
|
||||||
|
if _DjangoMiddleware._otel_request_hook:
|
||||||
|
_DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable
|
||||||
|
span, request
|
||||||
|
)
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def process_view(self, request, view_func, *args, **kwargs):
|
def process_view(self, request, view_func, *args, **kwargs):
|
||||||
# Process view is executed before the view function, here we get the
|
# Process view is executed before the view function, here we get the
|
||||||
@ -156,30 +169,30 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
|
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if (
|
activation = request.META.pop(self._environ_activation_key, None)
|
||||||
self._environ_activation_key in request.META.keys()
|
span = request.META.pop(self._environ_span_key, None)
|
||||||
and self._environ_span_key in request.META.keys()
|
|
||||||
):
|
if activation and span:
|
||||||
add_response_attributes(
|
add_response_attributes(
|
||||||
request.META[self._environ_span_key],
|
span,
|
||||||
"{} {}".format(response.status_code, response.reason_phrase),
|
"{} {}".format(response.status_code, response.reason_phrase),
|
||||||
response,
|
response,
|
||||||
)
|
)
|
||||||
|
|
||||||
request.META.pop(self._environ_span_key)
|
|
||||||
|
|
||||||
exception = request.META.pop(self._environ_exception_key, None)
|
exception = request.META.pop(self._environ_exception_key, None)
|
||||||
|
if _DjangoMiddleware._otel_response_hook:
|
||||||
|
_DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable
|
||||||
|
span, request, response
|
||||||
|
)
|
||||||
|
|
||||||
if exception:
|
if exception:
|
||||||
request.META[self._environ_activation_key].__exit__(
|
activation.__exit__(
|
||||||
type(exception),
|
type(exception),
|
||||||
exception,
|
exception,
|
||||||
getattr(exception, "__traceback__", None),
|
getattr(exception, "__traceback__", None),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
request.META[self._environ_activation_key].__exit__(
|
activation.__exit__(None, None, None)
|
||||||
None, None, None
|
|
||||||
)
|
|
||||||
request.META.pop(self._environ_activation_key)
|
|
||||||
|
|
||||||
if self._environ_token in request.META.keys():
|
if self._environ_token in request.META.keys():
|
||||||
detach(request.environ.get(self._environ_token))
|
detach(request.environ.get(self._environ_token))
|
||||||
|
@ -18,10 +18,15 @@ from unittest.mock import Mock, patch
|
|||||||
from django import VERSION
|
from django import VERSION
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||||
|
|
||||||
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
from opentelemetry.instrumentation.django import (
|
||||||
|
DjangoInstrumentor,
|
||||||
|
_DjangoMiddleware,
|
||||||
|
)
|
||||||
|
from opentelemetry.sdk.trace import Span
|
||||||
from opentelemetry.test.test_base import TestBase
|
from opentelemetry.test.test_base import TestBase
|
||||||
from opentelemetry.test.wsgitestutil import WsgiTestBase
|
from opentelemetry.test.wsgitestutil import WsgiTestBase
|
||||||
from opentelemetry.trace import SpanKind, StatusCode
|
from opentelemetry.trace import SpanKind, StatusCode
|
||||||
@ -268,3 +273,42 @@ class TestMiddleware(TestBase, WsgiTestBase):
|
|||||||
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
|
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
|
||||||
self.assertEqual(span.attributes["content_type"], "test/ct")
|
self.assertEqual(span.attributes["content_type"], "test/ct")
|
||||||
self.assertNotIn("non_existing_variable", span.attributes)
|
self.assertNotIn("non_existing_variable", span.attributes)
|
||||||
|
|
||||||
|
def test_hooks(self):
|
||||||
|
request_hook_args = ()
|
||||||
|
response_hook_args = ()
|
||||||
|
|
||||||
|
def request_hook(span, request):
|
||||||
|
nonlocal request_hook_args
|
||||||
|
request_hook_args = (span, request)
|
||||||
|
|
||||||
|
def response_hook(span, request, response):
|
||||||
|
nonlocal response_hook_args
|
||||||
|
response_hook_args = (span, request, response)
|
||||||
|
response["hook-header"] = "set by hook"
|
||||||
|
|
||||||
|
_DjangoMiddleware._otel_request_hook = request_hook
|
||||||
|
_DjangoMiddleware._otel_response_hook = response_hook
|
||||||
|
|
||||||
|
response = Client().get("/span_name/1234/")
|
||||||
|
_DjangoMiddleware._otel_request_hook = (
|
||||||
|
_DjangoMiddleware._otel_response_hook
|
||||||
|
) = None
|
||||||
|
|
||||||
|
self.assertEqual(response["hook-header"], "set by hook")
|
||||||
|
|
||||||
|
span_list = self.memory_exporter.get_finished_spans()
|
||||||
|
self.assertEqual(len(span_list), 1)
|
||||||
|
span = span_list[0]
|
||||||
|
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
|
||||||
|
|
||||||
|
self.assertEqual(len(request_hook_args), 2)
|
||||||
|
self.assertEqual(request_hook_args[0].name, span.name)
|
||||||
|
self.assertIsInstance(request_hook_args[0], Span)
|
||||||
|
self.assertIsInstance(request_hook_args[1], HttpRequest)
|
||||||
|
|
||||||
|
self.assertEqual(len(response_hook_args), 3)
|
||||||
|
self.assertEqual(request_hook_args[0], response_hook_args[0])
|
||||||
|
self.assertIsInstance(response_hook_args[1], HttpRequest)
|
||||||
|
self.assertIsInstance(response_hook_args[2], HttpResponse)
|
||||||
|
self.assertEqual(response_hook_args[2], response)
|
||||||
|
Reference in New Issue
Block a user