Skip to content

Commit ba1696a

Browse files
committed
Support request and resposne hooks for Django instrumentation
1 parent 92004b1 commit ba1696a

File tree

5 files changed

+155
-14
lines changed

5 files changed

+155
-14
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
### Added
1818
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
1919
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
20+
- `opentelemetry-instrumenation-django` now supports request and response hooks.
21+
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
2022

2123
## [0.19b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.19b0) - 2021-03-26
2224

instrumentation/opentelemetry-instrumentation-django/README.rst

+16
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ will extract path_info and content_type attributes from every traced request and
4545

4646
Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes
4747

48+
Request and Response hooks
49+
***************************
50+
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
51+
and right before the span is finished while processing a response. The hooks can be configured as follows:
52+
53+
::
54+
55+
def request_hook(span, request):
56+
pass
57+
58+
def response_hook(span, request, response):
59+
pass
60+
61+
DjangoInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
62+
63+
4864
References
4965
----------
5066

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py

+66
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,67 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
"""
15+
16+
Instrument `django`_ to trace Django applications.
17+
18+
.. _django: https://pypi.org/project/django/
19+
20+
Usage
21+
-----
22+
23+
.. code:: python
24+
25+
from opentelemetry.instrumentation.django import DjangoInstrumentor
26+
27+
DjangoInstrumentor().instrument()
28+
29+
30+
Configuration
31+
-------------
32+
33+
Exclude lists
34+
*************
35+
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.
36+
37+
For example,
38+
39+
::
40+
41+
export OTEL_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck"
42+
43+
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
44+
45+
Request attributes
46+
********************
47+
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
48+
delimited list of request attribute names.
49+
50+
For example,
51+
52+
::
53+
54+
export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type'
55+
56+
will extract path_info and content_type attributes from every traced request and add them as span attritbues.
57+
58+
Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes
59+
60+
Request and Response hooks
61+
***************************
62+
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
63+
and right before the span is finished while processing a response. The hooks can be configured as follows:
64+
65+
.. code:: python
66+
67+
def request_hook(span, request):
68+
pass
69+
70+
def response_hook(span, request, response):
71+
pass
72+
73+
DjangoInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
74+
"""
1475

1576
from logging import getLogger
1677
from os import environ
@@ -44,6 +105,11 @@ def _instrument(self, **kwargs):
44105
if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False":
45106
return
46107

108+
_DjangoMiddleware._otel_request_hook = kwargs.pop("request_hook", None)
109+
_DjangoMiddleware._otel_response_hook = kwargs.pop(
110+
"response_hook", None
111+
)
112+
47113
# This can not be solved, but is an inherent problem of this approach:
48114
# the order of middleware entries matters, and here you have no control
49115
# on that:

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py

+26-13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
from logging import getLogger
1616
from time import time
17+
from typing import Callable
18+
19+
from django.http import HttpRequest, HttpResponse
1720

1821
from opentelemetry.context import attach, detach
1922
from opentelemetry.instrumentation.django.version import __version__
@@ -24,7 +27,7 @@
2427
wsgi_getter,
2528
)
2629
from opentelemetry.propagate import extract
27-
from opentelemetry.trace import SpanKind, get_tracer, use_span
30+
from opentelemetry.trace import Span, SpanKind, get_tracer, use_span
2831
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
2932

3033
try:
@@ -62,6 +65,11 @@ class _DjangoMiddleware(MiddlewareMixin):
6265
_traced_request_attrs = get_traced_request_attrs("DJANGO")
6366
_excluded_urls = get_excluded_urls("DJANGO")
6467

68+
_otel_request_hook: Callable[[Span, HttpRequest], None] = None
69+
_otel_response_hook: Callable[
70+
[Span, HttpRequest, HttpResponse], None
71+
] = None
72+
6573
@staticmethod
6674
def _get_span_name(request):
6775
try:
@@ -125,6 +133,11 @@ def process_request(self, request):
125133
request.META[self._environ_span_key] = span
126134
request.META[self._environ_token] = token
127135

136+
if _DjangoMiddleware._otel_request_hook:
137+
_DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable
138+
span, request
139+
)
140+
128141
# pylint: disable=unused-argument
129142
def process_view(self, request, view_func, *args, **kwargs):
130143
# Process view is executed before the view function, here we get the
@@ -156,30 +169,30 @@ def process_response(self, request, response):
156169
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
157170
return response
158171

159-
if (
160-
self._environ_activation_key in request.META.keys()
161-
and self._environ_span_key in request.META.keys()
162-
):
172+
activation = request.META.pop(self._environ_activation_key, None)
173+
span = request.META.pop(self._environ_span_key, None)
174+
175+
if activation and span:
163176
add_response_attributes(
164-
request.META[self._environ_span_key],
177+
span,
165178
"{} {}".format(response.status_code, response.reason_phrase),
166179
response,
167180
)
168181

169-
request.META.pop(self._environ_span_key)
170-
171182
exception = request.META.pop(self._environ_exception_key, None)
183+
if _DjangoMiddleware._otel_response_hook:
184+
_DjangoMiddleware._otel_response_hook(
185+
span, request, response
186+
) # pylint: disable=not-callable
187+
172188
if exception:
173-
request.META[self._environ_activation_key].__exit__(
189+
activation.__exit__(
174190
type(exception),
175191
exception,
176192
getattr(exception, "__traceback__", None),
177193
)
178194
else:
179-
request.META[self._environ_activation_key].__exit__(
180-
None, None, None
181-
)
182-
request.META.pop(self._environ_activation_key)
195+
activation.__exit__(None, None, None)
183196

184197
if self._environ_token in request.META.keys():
185198
detach(request.environ.get(self._environ_token))

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py

+45-1
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818
from django import VERSION
1919
from django.conf import settings
2020
from django.conf.urls import url
21+
from django.http import HttpRequest, HttpResponse
2122
from django.test import Client
2223
from django.test.utils import setup_test_environment, teardown_test_environment
2324

24-
from opentelemetry.instrumentation.django import DjangoInstrumentor
25+
from opentelemetry.instrumentation.django import (
26+
DjangoInstrumentor,
27+
_DjangoMiddleware,
28+
)
29+
from opentelemetry.sdk.trace import Span
2530
from opentelemetry.test.test_base import TestBase
2631
from opentelemetry.test.wsgitestutil import WsgiTestBase
2732
from opentelemetry.trace import SpanKind, StatusCode
@@ -268,3 +273,42 @@ def test_traced_request_attrs(self):
268273
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
269274
self.assertEqual(span.attributes["content_type"], "test/ct")
270275
self.assertNotIn("non_existing_variable", span.attributes)
276+
277+
def test_hooks(self):
278+
request_hook_args = ()
279+
response_hook_args = ()
280+
281+
def request_hook(span, request):
282+
nonlocal request_hook_args
283+
request_hook_args = (span, request)
284+
285+
def response_hook(span, request, response):
286+
nonlocal response_hook_args
287+
response_hook_args = (span, request, response)
288+
response["hook-header"] = "set by hook"
289+
290+
_DjangoMiddleware._otel_request_hook = request_hook
291+
_DjangoMiddleware._otel_response_hook = response_hook
292+
293+
response = Client().get("/span_name/1234/")
294+
_DjangoMiddleware._otel_request_hook = (
295+
_DjangoMiddleware._otel_response_hook
296+
) = None
297+
298+
self.assertEqual(response["hook-header"], "set by hook")
299+
300+
span_list = self.memory_exporter.get_finished_spans()
301+
self.assertEqual(len(span_list), 1)
302+
span = span_list[0]
303+
self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
304+
305+
self.assertEqual(len(request_hook_args), 2)
306+
self.assertEqual(request_hook_args[0].name, span.name)
307+
self.assertIsInstance(request_hook_args[0], Span)
308+
self.assertIsInstance(request_hook_args[1], HttpRequest)
309+
310+
self.assertEqual(len(response_hook_args), 3)
311+
self.assertEqual(request_hook_args[0], response_hook_args[0])
312+
self.assertIsInstance(response_hook_args[1], HttpRequest)
313+
self.assertIsInstance(response_hook_args[2], HttpResponse)
314+
self.assertEqual(response_hook_args[2], response)

0 commit comments

Comments
 (0)