Skip to content

Commit 7382b5e

Browse files
Restore metrics in django (#1208)
1 parent c2f9ebe commit 7382b5e

File tree

6 files changed

+131
-8
lines changed

6 files changed

+131
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies.
1313
([#1234](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1234))
1414

15+
- restoring metrics in django framework
16+
([#1208](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1208))
17+
1518
## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0-0.33b0) - 2022-08-08
1619

1720
- Adding multiple db connections support for django-instrumentation's sqlcommenter

instrumentation/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
| [opentelemetry-instrumentation-celery](./opentelemetry-instrumentation-celery) | celery >= 4.0, < 6.0 | No
1414
| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka ~= 1.8.2 | No
1515
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No
16-
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | No
16+
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes
1717
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No
1818
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | No
1919
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | No

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ def response_hook(span, request, response):
205205
from opentelemetry.instrumentation.django.package import _instruments
206206
from opentelemetry.instrumentation.django.version import __version__
207207
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
208+
from opentelemetry.metrics import get_meter
208209
from opentelemetry.trace import get_tracer
209210

210211
DJANGO_2_0 = django_version >= (2, 0)
@@ -244,19 +245,29 @@ def _instrument(self, **kwargs):
244245
return
245246

246247
tracer_provider = kwargs.get("tracer_provider")
248+
meter_provider = kwargs.get("meter_provider")
247249
tracer = get_tracer(
248250
__name__,
249251
__version__,
250252
tracer_provider=tracer_provider,
251253
)
252-
254+
meter = get_meter(__name__, __version__, meter_provider=meter_provider)
253255
_DjangoMiddleware._tracer = tracer
254-
256+
_DjangoMiddleware._meter = meter
255257
_DjangoMiddleware._otel_request_hook = kwargs.pop("request_hook", None)
256258
_DjangoMiddleware._otel_response_hook = kwargs.pop(
257259
"response_hook", None
258260
)
259-
261+
_DjangoMiddleware._duration_histogram = meter.create_histogram(
262+
name="http.server.duration",
263+
unit="ms",
264+
description="measures the duration of the inbound http request",
265+
)
266+
_DjangoMiddleware._active_request_counter = meter.create_up_down_counter(
267+
name="http.server.active_requests",
268+
unit="requests",
269+
description="measures the number of concurent HTTP requests those are currently in flight",
270+
)
260271
# This can not be solved, but is an inherent problem of this approach:
261272
# the order of middleware entries matters, and here you have no control
262273
# on that:

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

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import types
1616
from logging import getLogger
1717
from time import time
18+
from timeit import default_timer
1819
from typing import Callable
1920

2021
from django import VERSION as django_version
@@ -41,7 +42,12 @@
4142
from opentelemetry.instrumentation.wsgi import wsgi_getter
4243
from opentelemetry.semconv.trace import SpanAttributes
4344
from opentelemetry.trace import Span, SpanKind, use_span
44-
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
45+
from opentelemetry.util.http import (
46+
_parse_active_request_count_attrs,
47+
_parse_duration_attrs,
48+
get_excluded_urls,
49+
get_traced_request_attrs,
50+
)
4551

4652
try:
4753
from django.core.urlresolvers import ( # pylint: disable=no-name-in-module
@@ -139,10 +145,19 @@ class _DjangoMiddleware(MiddlewareMixin):
139145
_environ_token = "opentelemetry-instrumentor-django.token"
140146
_environ_span_key = "opentelemetry-instrumentor-django.span_key"
141147
_environ_exception_key = "opentelemetry-instrumentor-django.exception_key"
142-
148+
_environ_active_request_attr_key = (
149+
"opentelemetry-instrumentor-django.active_request_attr_key"
150+
)
151+
_environ_duration_attr_key = (
152+
"opentelemetry-instrumentor-django.duration_attr_key"
153+
)
154+
_environ_timer_key = "opentelemetry-instrumentor-django.timer_key"
143155
_traced_request_attrs = get_traced_request_attrs("DJANGO")
144156
_excluded_urls = get_excluded_urls("DJANGO")
145157
_tracer = None
158+
_meter = None
159+
_duration_histogram = None
160+
_active_request_counter = None
146161

147162
_otel_request_hook: Callable[[Span, HttpRequest], None] = None
148163
_otel_response_hook: Callable[
@@ -171,6 +186,7 @@ def _get_span_name(request):
171186
except Resolver404:
172187
return f"HTTP {request.method}"
173188

189+
# pylint: disable=too-many-locals
174190
def process_request(self, request):
175191
# request.META is a dictionary containing all available HTTP headers
176192
# Read more about request.META here:
@@ -185,7 +201,6 @@ def process_request(self, request):
185201

186202
# pylint:disable=W0212
187203
request._otel_start_time = time()
188-
189204
request_meta = request.META
190205

191206
if is_asgi_request:
@@ -208,7 +223,16 @@ def process_request(self, request):
208223
)
209224

210225
attributes = collect_request_attributes(carrier)
226+
active_requests_count_attrs = _parse_active_request_count_attrs(
227+
attributes
228+
)
229+
duration_attrs = _parse_duration_attrs(attributes)
211230

231+
request.META[
232+
self._environ_active_request_attr_key
233+
] = active_requests_count_attrs
234+
request.META[self._environ_duration_attr_key] = duration_attrs
235+
self._active_request_counter.add(1, active_requests_count_attrs)
212236
if span.is_recording():
213237
attributes = extract_attributes_from_object(
214238
request, self._traced_request_attrs, attributes
@@ -242,7 +266,8 @@ def process_request(self, request):
242266

243267
activation = use_span(span, end_on_exit=True)
244268
activation.__enter__() # pylint: disable=E1101
245-
269+
request_start_time = default_timer()
270+
request.META[self._environ_timer_key] = request_start_time
246271
request.META[self._environ_activation_key] = activation
247272
request.META[self._environ_span_key] = span
248273
if token:
@@ -281,6 +306,7 @@ def process_exception(self, request, exception):
281306
request.META[self._environ_exception_key] = exception
282307

283308
# pylint: disable=too-many-branches
309+
# pylint: disable=too-many-locals
284310
def process_response(self, request, response):
285311
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
286312
return response
@@ -291,6 +317,17 @@ def process_response(self, request, response):
291317

292318
activation = request.META.pop(self._environ_activation_key, None)
293319
span = request.META.pop(self._environ_span_key, None)
320+
active_requests_count_attrs = request.META.pop(
321+
self._environ_active_request_attr_key, None
322+
)
323+
duration_attrs = request.META.pop(
324+
self._environ_duration_attr_key, None
325+
)
326+
if duration_attrs:
327+
duration_attrs[
328+
SpanAttributes.HTTP_STATUS_CODE
329+
] = response.status_code
330+
request_start_time = request.META.pop(self._environ_timer_key, None)
294331

295332
if activation and span:
296333
if is_asgi_request:
@@ -341,6 +378,12 @@ def process_response(self, request, response):
341378
else:
342379
activation.__exit__(None, None, None)
343380

381+
if request_start_time is not None:
382+
duration = max(
383+
round((default_timer() - request_start_time) * 1000), 0
384+
)
385+
self._duration_histogram.record(duration, duration_attrs)
386+
self._active_request_counter.add(-1, active_requests_count_attrs)
344387
if request.META.get(self._environ_token, None) is not None:
345388
detach(request.META.get(self._environ_token))
346389
request.META.pop(self._environ_token)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414

1515

1616
_instruments = ("django >= 1.10",)
17+
_supports_metrics = True

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# pylint: disable=E0611
1616

1717
from sys import modules
18+
from timeit import default_timer
1819
from unittest.mock import Mock, patch
1920

2021
from django import VERSION, conf
@@ -32,6 +33,10 @@
3233
set_global_response_propagator,
3334
)
3435
from opentelemetry.sdk import resources
36+
from opentelemetry.sdk.metrics.export import (
37+
HistogramDataPoint,
38+
NumberDataPoint,
39+
)
3540
from opentelemetry.sdk.trace import Span
3641
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
3742
from opentelemetry.semconv.trace import SpanAttributes
@@ -45,6 +50,8 @@
4550
from opentelemetry.util.http import (
4651
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
4752
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
53+
_active_requests_count_attrs,
54+
_duration_attrs,
4855
get_excluded_urls,
4956
get_traced_request_attrs,
5057
)
@@ -406,6 +413,64 @@ def test_trace_response_headers(self):
406413
)
407414
self.memory_exporter.clear()
408415

416+
# pylint: disable=too-many-locals
417+
def test_wsgi_metrics(self):
418+
_expected_metric_names = [
419+
"http.server.active_requests",
420+
"http.server.duration",
421+
]
422+
_recommended_attrs = {
423+
"http.server.active_requests": _active_requests_count_attrs,
424+
"http.server.duration": _duration_attrs,
425+
}
426+
start = default_timer()
427+
for _ in range(3):
428+
response = Client().get("/span_name/1234/")
429+
self.assertEqual(response.status_code, 200)
430+
duration = max(round((default_timer() - start) * 1000), 0)
431+
metrics_list = self.memory_metrics_reader.get_metrics_data()
432+
number_data_point_seen = False
433+
histrogram_data_point_seen = False
434+
435+
self.assertTrue(len(metrics_list.resource_metrics) != 0)
436+
for resource_metric in metrics_list.resource_metrics:
437+
self.assertTrue(len(resource_metric.scope_metrics) != 0)
438+
for scope_metric in resource_metric.scope_metrics:
439+
self.assertTrue(len(scope_metric.metrics) != 0)
440+
for metric in scope_metric.metrics:
441+
self.assertIn(metric.name, _expected_metric_names)
442+
data_points = list(metric.data.data_points)
443+
self.assertEqual(len(data_points), 1)
444+
for point in data_points:
445+
if isinstance(point, HistogramDataPoint):
446+
self.assertEqual(point.count, 3)
447+
histrogram_data_point_seen = True
448+
self.assertAlmostEqual(
449+
duration, point.sum, delta=100
450+
)
451+
if isinstance(point, NumberDataPoint):
452+
number_data_point_seen = True
453+
self.assertEqual(point.value, 0)
454+
for attr in point.attributes:
455+
self.assertIn(
456+
attr, _recommended_attrs[metric.name]
457+
)
458+
self.assertTrue(histrogram_data_point_seen and number_data_point_seen)
459+
460+
def test_wsgi_metrics_unistrument(self):
461+
Client().get("/span_name/1234/")
462+
_django_instrumentor.uninstrument()
463+
Client().get("/span_name/1234/")
464+
metrics_list = self.memory_metrics_reader.get_metrics_data()
465+
for resource_metric in metrics_list.resource_metrics:
466+
for scope_metric in resource_metric.scope_metrics:
467+
for metric in scope_metric.metrics:
468+
for point in list(metric.data.data_points):
469+
if isinstance(point, HistogramDataPoint):
470+
self.assertEqual(1, point.count)
471+
if isinstance(point, NumberDataPoint):
472+
self.assertEqual(0, point.value)
473+
409474

410475
class TestMiddlewareWithTracerProvider(WsgiTestBase):
411476
@classmethod

0 commit comments

Comments
 (0)