mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-08-02 19:47:17 +08:00
Adding metric collection as part of instrumentations - Django (#1230)
This commit is contained in:
@ -11,6 +11,8 @@ Released 2020-10-13
|
|||||||
- Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992))
|
- Changed span name extraction from request to comply semantic convention ([#992](https://github.com/open-telemetry/opentelemetry-python/pull/992))
|
||||||
- Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154))
|
- Added support for `OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS` ([#1154](https://github.com/open-telemetry/opentelemetry-python/pull/1154))
|
||||||
- Added capture of http.route ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226))
|
- Added capture of http.route ([#1226](https://github.com/open-telemetry/opentelemetry-python/issues/1226))
|
||||||
|
- Add support for tracking http metrics
|
||||||
|
([#1230](https://github.com/open-telemetry/opentelemetry-python/pull/1230))
|
||||||
|
|
||||||
## Version 0.13b0
|
## Version 0.13b0
|
||||||
|
|
||||||
|
@ -18,12 +18,18 @@ from django.conf import settings
|
|||||||
|
|
||||||
from opentelemetry.configuration import Configuration
|
from opentelemetry.configuration import Configuration
|
||||||
from opentelemetry.instrumentation.django.middleware import _DjangoMiddleware
|
from opentelemetry.instrumentation.django.middleware import _DjangoMiddleware
|
||||||
|
from opentelemetry.instrumentation.django.version import __version__
|
||||||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||||
|
from opentelemetry.instrumentation.metric import (
|
||||||
|
HTTPMetricRecorder,
|
||||||
|
HTTPMetricType,
|
||||||
|
MetricMixin,
|
||||||
|
)
|
||||||
|
|
||||||
_logger = getLogger(__name__)
|
_logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DjangoInstrumentor(BaseInstrumentor):
|
class DjangoInstrumentor(BaseInstrumentor, MetricMixin):
|
||||||
"""An instrumentor for Django
|
"""An instrumentor for Django
|
||||||
|
|
||||||
See `BaseInstrumentor`
|
See `BaseInstrumentor`
|
||||||
@ -57,6 +63,11 @@ class DjangoInstrumentor(BaseInstrumentor):
|
|||||||
settings_middleware = list(settings_middleware)
|
settings_middleware = list(settings_middleware)
|
||||||
|
|
||||||
settings_middleware.insert(0, self._opentelemetry_middleware)
|
settings_middleware.insert(0, self._opentelemetry_middleware)
|
||||||
|
self.init_metrics(
|
||||||
|
__name__, __version__,
|
||||||
|
)
|
||||||
|
metric_recorder = HTTPMetricRecorder(self.meter, HTTPMetricType.SERVER)
|
||||||
|
setattr(settings, "OTEL_METRIC_RECORDER", metric_recorder)
|
||||||
setattr(settings, "MIDDLEWARE", settings_middleware)
|
setattr(settings, "MIDDLEWARE", settings_middleware)
|
||||||
|
|
||||||
def _uninstrument(self, **kwargs):
|
def _uninstrument(self, **kwargs):
|
||||||
|
@ -12,8 +12,11 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
import time
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from opentelemetry.configuration import Configuration
|
from opentelemetry.configuration import Configuration
|
||||||
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__
|
||||||
@ -41,11 +44,16 @@ except ImportError:
|
|||||||
MiddlewareMixin = object
|
MiddlewareMixin = object
|
||||||
|
|
||||||
_logger = getLogger(__name__)
|
_logger = getLogger(__name__)
|
||||||
|
_attributes_by_preference = [
|
||||||
|
["http.scheme", "http.host", "http.target"],
|
||||||
|
["http.scheme", "http.server_name", "net.host.port", "http.target"],
|
||||||
|
["http.scheme", "net.host.name", "net.host.port", "http.target"],
|
||||||
|
["http.url"],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class _DjangoMiddleware(MiddlewareMixin):
|
class _DjangoMiddleware(MiddlewareMixin):
|
||||||
"""Django Middleware for OpenTelemetry
|
"""Django Middleware for OpenTelemetry"""
|
||||||
"""
|
|
||||||
|
|
||||||
_environ_activation_key = (
|
_environ_activation_key = (
|
||||||
"opentelemetry-instrumentor-django.activation_key"
|
"opentelemetry-instrumentor-django.activation_key"
|
||||||
@ -88,6 +96,21 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
except Resolver404:
|
except Resolver404:
|
||||||
return "HTTP {}".format(request.method)
|
return "HTTP {}".format(request.method)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_metric_labels_from_attributes(attributes):
|
||||||
|
labels = {}
|
||||||
|
labels["http.method"] = attributes.get("http.method", "")
|
||||||
|
for attrs in _attributes_by_preference:
|
||||||
|
labels_from_attributes = {
|
||||||
|
attr: attributes.get(attr, None) for attr in attrs
|
||||||
|
}
|
||||||
|
if set(attrs).issubset(attributes.keys()):
|
||||||
|
labels.update(labels_from_attributes)
|
||||||
|
break
|
||||||
|
if attributes.get("http.flavor"):
|
||||||
|
labels["http.flavor"] = attributes.get("http.flavor")
|
||||||
|
return labels
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
# request.META is a dictionary containing all available HTTP headers
|
# request.META is a dictionary containing all available HTTP headers
|
||||||
# Read more about request.META here:
|
# Read more about request.META here:
|
||||||
@ -96,6 +119,9 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
|
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# pylint:disable=W0212
|
||||||
|
request._otel_start_time = time.time()
|
||||||
|
|
||||||
environ = request.META
|
environ = request.META
|
||||||
|
|
||||||
token = attach(extract(get_header_from_environ, environ))
|
token = attach(extract(get_header_from_environ, environ))
|
||||||
@ -110,8 +136,13 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if span.is_recording():
|
|
||||||
attributes = collect_request_attributes(environ)
|
attributes = collect_request_attributes(environ)
|
||||||
|
# pylint:disable=W0212
|
||||||
|
request._otel_labels = self._get_metric_labels_from_attributes(
|
||||||
|
attributes
|
||||||
|
)
|
||||||
|
|
||||||
|
if span.is_recording():
|
||||||
attributes = extract_attributes_from_object(
|
attributes = extract_attributes_from_object(
|
||||||
request, self._traced_request_attrs, attributes
|
request, self._traced_request_attrs, attributes
|
||||||
)
|
)
|
||||||
@ -176,6 +207,10 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
"{} {}".format(response.status_code, response.reason_phrase),
|
"{} {}".format(response.status_code, response.reason_phrase),
|
||||||
response,
|
response,
|
||||||
)
|
)
|
||||||
|
# pylint:disable=W0212
|
||||||
|
request._otel_labels["http.status_code"] = str(
|
||||||
|
response.status_code
|
||||||
|
)
|
||||||
request.META.pop(self._environ_span_key)
|
request.META.pop(self._environ_span_key)
|
||||||
|
|
||||||
request.META[self._environ_activation_key].__exit__(
|
request.META[self._environ_activation_key].__exit__(
|
||||||
@ -187,4 +222,14 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
detach(request.environ.get(self._environ_token))
|
detach(request.environ.get(self._environ_token))
|
||||||
request.META.pop(self._environ_token)
|
request.META.pop(self._environ_token)
|
||||||
|
|
||||||
|
try:
|
||||||
|
metric_recorder = getattr(settings, "OTEL_METRIC_RECORDER", None)
|
||||||
|
if metric_recorder is not None:
|
||||||
|
# pylint:disable=W0212
|
||||||
|
metric_recorder.record_server_duration_range(
|
||||||
|
request._otel_start_time, time.time(), request._otel_labels
|
||||||
|
)
|
||||||
|
except Exception as ex: # pylint: disable=W0703
|
||||||
|
_logger.warning("Error recording duration metrics: %s", ex)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -23,6 +23,8 @@ from django.test.utils import setup_test_environment, teardown_test_environment
|
|||||||
|
|
||||||
from opentelemetry.configuration import Configuration
|
from opentelemetry.configuration import Configuration
|
||||||
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||||
|
from opentelemetry.sdk.util import get_dict_as_key
|
||||||
|
from opentelemetry.test.test_base import TestBase
|
||||||
from opentelemetry.test.wsgitestutil import WsgiTestBase
|
from opentelemetry.test.wsgitestutil import WsgiTestBase
|
||||||
from opentelemetry.trace import SpanKind
|
from opentelemetry.trace import SpanKind
|
||||||
from opentelemetry.trace.status import StatusCanonicalCode
|
from opentelemetry.trace.status import StatusCanonicalCode
|
||||||
@ -53,7 +55,7 @@ urlpatterns = [
|
|||||||
_django_instrumentor = DjangoInstrumentor()
|
_django_instrumentor = DjangoInstrumentor()
|
||||||
|
|
||||||
|
|
||||||
class TestMiddleware(WsgiTestBase):
|
class TestMiddleware(TestBase, WsgiTestBase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
@ -121,6 +123,26 @@ class TestMiddleware(WsgiTestBase):
|
|||||||
self.assertEqual(span.attributes["http.status_code"], 200)
|
self.assertEqual(span.attributes["http.status_code"], 200)
|
||||||
self.assertEqual(span.attributes["http.status_text"], "OK")
|
self.assertEqual(span.attributes["http.status_text"], "OK")
|
||||||
|
|
||||||
|
self.assertIsNotNone(_django_instrumentor.meter)
|
||||||
|
self.assertEqual(len(_django_instrumentor.meter.metrics), 1)
|
||||||
|
recorder = _django_instrumentor.meter.metrics.pop()
|
||||||
|
match_key = get_dict_as_key(
|
||||||
|
{
|
||||||
|
"http.flavor": "1.1",
|
||||||
|
"http.method": "GET",
|
||||||
|
"http.status_code": "200",
|
||||||
|
"http.url": "http://testserver/traced/",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for key in recorder.bound_instruments.keys():
|
||||||
|
self.assertEqual(key, match_key)
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
bound = recorder.bound_instruments.get(key)
|
||||||
|
for view_data in bound.view_datas:
|
||||||
|
self.assertEqual(view_data.labels, key)
|
||||||
|
self.assertEqual(view_data.aggregator.current.count, 1)
|
||||||
|
self.assertGreaterEqual(view_data.aggregator.current.sum, 0)
|
||||||
|
|
||||||
def test_not_recording(self):
|
def test_not_recording(self):
|
||||||
mock_tracer = Mock()
|
mock_tracer = Mock()
|
||||||
mock_span = Mock()
|
mock_span = Mock()
|
||||||
@ -180,6 +202,23 @@ class TestMiddleware(WsgiTestBase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(span.attributes["http.route"], "^error/")
|
self.assertEqual(span.attributes["http.route"], "^error/")
|
||||||
self.assertEqual(span.attributes["http.scheme"], "http")
|
self.assertEqual(span.attributes["http.scheme"], "http")
|
||||||
|
self.assertIsNotNone(_django_instrumentor.meter)
|
||||||
|
self.assertEqual(len(_django_instrumentor.meter.metrics), 1)
|
||||||
|
recorder = _django_instrumentor.meter.metrics.pop()
|
||||||
|
match_key = get_dict_as_key(
|
||||||
|
{
|
||||||
|
"http.flavor": "1.1",
|
||||||
|
"http.method": "GET",
|
||||||
|
"http.url": "http://testserver/error/",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for key in recorder.bound_instruments.keys():
|
||||||
|
self.assertEqual(key, match_key)
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
bound = recorder.bound_instruments.get(key)
|
||||||
|
for view_data in bound.view_datas:
|
||||||
|
self.assertEqual(view_data.labels, key)
|
||||||
|
self.assertEqual(view_data.aggregator.current.count, 1)
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
|
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
|
||||||
|
Reference in New Issue
Block a user