mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-29 21:23:55 +08:00
Add Django ASGI support (#391)
This commit is contained in:

committed by
GitHub

parent
36275f3cbf
commit
5105820fff
@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
([#706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/706))
|
||||
- `opentelemetry-instrumentation-requests` added exclude urls functionality
|
||||
([#714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/714))
|
||||
- `opentelemetry-instrumentation-django` Add ASGI support
|
||||
([#391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/391))
|
||||
|
||||
### Changed
|
||||
- `opentelemetry-instrumentation-botocore` Make common span attributes compliant with semantic conventions
|
||||
@ -64,12 +66,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
- `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package
|
||||
([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586))
|
||||
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`,
|
||||
`opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks
|
||||
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`,
|
||||
`opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks
|
||||
([#576](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/576))
|
||||
- `opentelemetry-instrumentation-pika` added RabbitMQ's pika module instrumentation.
|
||||
([#680](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/680))
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- `opentelemetry-instrumentation-fastapi` Allow instrumentation of newer FastAPI versions.
|
||||
|
@ -121,7 +121,7 @@ def get_host_port_url_tuple(scope):
|
||||
"""Returns (host, port, full_url) tuple."""
|
||||
server = scope.get("server") or ["0.0.0.0", 80]
|
||||
port = server[1]
|
||||
server_host = server[0] + (":" + str(port) if port != 80 else "")
|
||||
server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
|
||||
full_path = scope.get("root_path", "") + scope.get("path", "")
|
||||
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
|
||||
return server_host, port, http_url
|
||||
|
@ -45,6 +45,8 @@ install_requires =
|
||||
opentelemetry-semantic-conventions == 0.24b0
|
||||
|
||||
[options.extras_require]
|
||||
asgi =
|
||||
opentelemetry-instrumentation-asgi == 0.24b0
|
||||
test =
|
||||
opentelemetry-test == 0.24b0
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import types
|
||||
from logging import getLogger
|
||||
from time import time
|
||||
from typing import Callable
|
||||
@ -24,11 +25,11 @@ from opentelemetry.instrumentation.propagators import (
|
||||
get_global_response_propagator,
|
||||
)
|
||||
from opentelemetry.instrumentation.utils import extract_attributes_from_object
|
||||
from opentelemetry.instrumentation.wsgi import add_response_attributes
|
||||
from opentelemetry.instrumentation.wsgi import (
|
||||
add_response_attributes,
|
||||
collect_request_attributes,
|
||||
wsgi_getter,
|
||||
collect_request_attributes as wsgi_collect_request_attributes,
|
||||
)
|
||||
from opentelemetry.instrumentation.wsgi import wsgi_getter
|
||||
from opentelemetry.propagate import extract
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
from opentelemetry.trace import Span, SpanKind, use_span
|
||||
@ -43,6 +44,7 @@ except ImportError:
|
||||
from django.urls import Resolver404, resolve
|
||||
|
||||
DJANGO_2_0 = django_version >= (2, 0)
|
||||
DJANGO_3_0 = django_version >= (3, 0)
|
||||
|
||||
if DJANGO_2_0:
|
||||
# Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style
|
||||
@ -67,6 +69,26 @@ else:
|
||||
except ImportError:
|
||||
MiddlewareMixin = object
|
||||
|
||||
if DJANGO_3_0:
|
||||
from django.core.handlers.asgi import ASGIRequest
|
||||
else:
|
||||
ASGIRequest = None
|
||||
|
||||
# try/except block exclusive for optional ASGI imports.
|
||||
try:
|
||||
from opentelemetry.instrumentation.asgi import asgi_getter
|
||||
from opentelemetry.instrumentation.asgi import (
|
||||
collect_request_attributes as asgi_collect_request_attributes,
|
||||
)
|
||||
from opentelemetry.instrumentation.asgi import set_status_code
|
||||
|
||||
_is_asgi_supported = True
|
||||
except ImportError:
|
||||
asgi_getter = None
|
||||
asgi_collect_request_attributes = None
|
||||
set_status_code = None
|
||||
_is_asgi_supported = False
|
||||
|
||||
|
||||
_logger = getLogger(__name__)
|
||||
_attributes_by_preference = [
|
||||
@ -91,6 +113,10 @@ _attributes_by_preference = [
|
||||
]
|
||||
|
||||
|
||||
def _is_asgi_request(request: HttpRequest) -> bool:
|
||||
return ASGIRequest is not None and isinstance(request, ASGIRequest)
|
||||
|
||||
|
||||
class _DjangoMiddleware(MiddlewareMixin):
|
||||
"""Django Middleware for OpenTelemetry"""
|
||||
|
||||
@ -140,12 +166,25 @@ class _DjangoMiddleware(MiddlewareMixin):
|
||||
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
|
||||
return
|
||||
|
||||
is_asgi_request = _is_asgi_request(request)
|
||||
if not _is_asgi_supported and is_asgi_request:
|
||||
return
|
||||
|
||||
# pylint:disable=W0212
|
||||
request._otel_start_time = time()
|
||||
|
||||
request_meta = request.META
|
||||
|
||||
token = attach(extract(request_meta, getter=wsgi_getter))
|
||||
if is_asgi_request:
|
||||
carrier = request.scope
|
||||
carrier_getter = asgi_getter
|
||||
collect_request_attributes = asgi_collect_request_attributes
|
||||
else:
|
||||
carrier = request_meta
|
||||
carrier_getter = wsgi_getter
|
||||
collect_request_attributes = wsgi_collect_request_attributes
|
||||
|
||||
token = attach(extract(request_meta, getter=carrier_getter))
|
||||
|
||||
span = self._tracer.start_span(
|
||||
self._get_span_name(request),
|
||||
@ -155,12 +194,25 @@ class _DjangoMiddleware(MiddlewareMixin):
|
||||
),
|
||||
)
|
||||
|
||||
attributes = collect_request_attributes(request_meta)
|
||||
attributes = collect_request_attributes(carrier)
|
||||
|
||||
if span.is_recording():
|
||||
attributes = extract_attributes_from_object(
|
||||
request, self._traced_request_attrs, attributes
|
||||
)
|
||||
if is_asgi_request:
|
||||
# ASGI requests include extra attributes in request.scope.headers.
|
||||
attributes = extract_attributes_from_object(
|
||||
types.SimpleNamespace(
|
||||
**{
|
||||
name.decode("latin1"): value.decode("latin1")
|
||||
for name, value in request.scope.get("headers", [])
|
||||
}
|
||||
),
|
||||
self._traced_request_attrs,
|
||||
attributes,
|
||||
)
|
||||
|
||||
for key, value in attributes.items():
|
||||
span.set_attribute(key, value)
|
||||
|
||||
@ -207,15 +259,22 @@ class _DjangoMiddleware(MiddlewareMixin):
|
||||
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
|
||||
return response
|
||||
|
||||
is_asgi_request = _is_asgi_request(request)
|
||||
if not _is_asgi_supported and is_asgi_request:
|
||||
return response
|
||||
|
||||
activation = request.META.pop(self._environ_activation_key, None)
|
||||
span = request.META.pop(self._environ_span_key, None)
|
||||
|
||||
if activation and span:
|
||||
add_response_attributes(
|
||||
span,
|
||||
f"{response.status_code} {response.reason_phrase}",
|
||||
response,
|
||||
)
|
||||
if is_asgi_request:
|
||||
set_status_code(span, response.status_code)
|
||||
else:
|
||||
add_response_attributes(
|
||||
span,
|
||||
f"{response.status_code} {response.reason_phrase}",
|
||||
response,
|
||||
)
|
||||
|
||||
propagator = get_global_response_propagator()
|
||||
if propagator:
|
||||
@ -238,7 +297,7 @@ class _DjangoMiddleware(MiddlewareMixin):
|
||||
activation.__exit__(None, None, None)
|
||||
|
||||
if self._environ_token in request.META.keys():
|
||||
detach(request.environ.get(self._environ_token))
|
||||
detach(request.META.get(self._environ_token))
|
||||
request.META.pop(self._environ_token)
|
||||
|
||||
return response
|
||||
|
@ -15,12 +15,11 @@
|
||||
from sys import modules
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django import VERSION
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django import VERSION, conf
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.test import Client
|
||||
from django.test.client import Client
|
||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||
from django.urls import re_path
|
||||
|
||||
from opentelemetry.instrumentation.django import (
|
||||
DjangoInstrumentor,
|
||||
@ -57,13 +56,13 @@ from .views import (
|
||||
DJANGO_2_2 = VERSION >= (2, 2)
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^traced/", traced),
|
||||
url(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
|
||||
url(r"^error/", error),
|
||||
url(r"^excluded_arg/", excluded),
|
||||
url(r"^excluded_noarg/", excluded_noarg),
|
||||
url(r"^excluded_noarg2/", excluded_noarg2),
|
||||
url(r"^span_name/([0-9]{4})/$", route_span_name),
|
||||
re_path(r"^traced/", traced),
|
||||
re_path(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
|
||||
re_path(r"^error/", error),
|
||||
re_path(r"^excluded_arg/", excluded),
|
||||
re_path(r"^excluded_noarg/", excluded_noarg),
|
||||
re_path(r"^excluded_noarg2/", excluded_noarg2),
|
||||
re_path(r"^span_name/([0-9]{4})/$", route_span_name),
|
||||
]
|
||||
_django_instrumentor = DjangoInstrumentor()
|
||||
|
||||
@ -71,8 +70,8 @@ _django_instrumentor = DjangoInstrumentor()
|
||||
class TestMiddleware(TestBase, WsgiTestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
conf.settings.configure(ROOT_URLCONF=modules[__name__])
|
||||
super().setUpClass()
|
||||
settings.configure(ROOT_URLCONF=modules[__name__])
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@ -105,6 +104,11 @@ class TestMiddleware(TestBase, WsgiTestBase):
|
||||
teardown_test_environment()
|
||||
_django_instrumentor.uninstrument()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
conf.settings = conf.LazySettings()
|
||||
|
||||
def test_templated_route_get(self):
|
||||
Client().get("/route/2020/template/")
|
||||
|
||||
@ -357,6 +361,7 @@ class TestMiddleware(TestBase, WsgiTestBase):
|
||||
class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
conf.settings.configure(ROOT_URLCONF=modules[__name__])
|
||||
super().setUpClass()
|
||||
|
||||
def setUp(self):
|
||||
@ -375,6 +380,11 @@ class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase):
|
||||
teardown_test_environment()
|
||||
_django_instrumentor.uninstrument()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
conf.settings = conf.LazySettings()
|
||||
|
||||
def test_tracer_provider_traced(self):
|
||||
Client().post("/traced/")
|
||||
|
||||
|
@ -0,0 +1,382 @@
|
||||
# Copyright The OpenTelemetry Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from sys import modules
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from django import VERSION, conf
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||
from django.urls import re_path
|
||||
|
||||
from opentelemetry.instrumentation.django import (
|
||||
DjangoInstrumentor,
|
||||
_DjangoMiddleware,
|
||||
)
|
||||
from opentelemetry.instrumentation.propagators import (
|
||||
TraceResponsePropagator,
|
||||
set_global_response_propagator,
|
||||
)
|
||||
from opentelemetry.sdk import resources
|
||||
from opentelemetry.sdk.trace import Span
|
||||
from opentelemetry.semconv.trace import SpanAttributes
|
||||
from opentelemetry.test.test_base import TestBase
|
||||
from opentelemetry.trace import (
|
||||
SpanKind,
|
||||
StatusCode,
|
||||
format_span_id,
|
||||
format_trace_id,
|
||||
)
|
||||
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
|
||||
|
||||
# pylint: disable=import-error
|
||||
from .views import (
|
||||
async_error,
|
||||
async_excluded,
|
||||
async_excluded_noarg,
|
||||
async_excluded_noarg2,
|
||||
async_route_span_name,
|
||||
async_traced,
|
||||
async_traced_template,
|
||||
)
|
||||
|
||||
DJANGO_3_1 = VERSION >= (3, 1)
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r"^traced/", async_traced),
|
||||
re_path(r"^route/(?P<year>[0-9]{4})/template/$", async_traced_template),
|
||||
re_path(r"^error/", async_error),
|
||||
re_path(r"^excluded_arg/", async_excluded),
|
||||
re_path(r"^excluded_noarg/", async_excluded_noarg),
|
||||
re_path(r"^excluded_noarg2/", async_excluded_noarg2),
|
||||
re_path(r"^span_name/([0-9]{4})/$", async_route_span_name),
|
||||
]
|
||||
_django_instrumentor = DjangoInstrumentor()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not DJANGO_3_1, reason="AsyncClient implemented since Django 3.1"
|
||||
)
|
||||
class TestMiddlewareAsgi(SimpleTestCase, TestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
conf.settings.configure(ROOT_URLCONF=modules[__name__])
|
||||
super().setUpClass()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
setup_test_environment()
|
||||
_django_instrumentor.instrument()
|
||||
self.env_patch = patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "http://testserver/excluded_arg/123,excluded_noarg",
|
||||
"OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS": "path_info,content_type,non_existing_variable",
|
||||
},
|
||||
)
|
||||
self.env_patch.start()
|
||||
self.exclude_patch = patch(
|
||||
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
|
||||
get_excluded_urls("DJANGO"),
|
||||
)
|
||||
self.traced_patch = patch(
|
||||
"opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs",
|
||||
get_traced_request_attrs("DJANGO"),
|
||||
)
|
||||
self.exclude_patch.start()
|
||||
self.traced_patch.start()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.env_patch.stop()
|
||||
self.exclude_patch.stop()
|
||||
self.traced_patch.stop()
|
||||
teardown_test_environment()
|
||||
_django_instrumentor.uninstrument()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
conf.settings = conf.LazySettings()
|
||||
|
||||
async def test_templated_route_get(self):
|
||||
await self.async_client.get("/route/2020/template/")
|
||||
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^route/(?P<year>[0-9]{4})/template/$")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_URL],
|
||||
"http://127.0.0.1/route/2020/template/",
|
||||
)
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_ROUTE],
|
||||
"^route/(?P<year>[0-9]{4})/template/$",
|
||||
)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
|
||||
|
||||
async def test_traced_get(self):
|
||||
await self.async_client.get("/traced/")
|
||||
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^traced/")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_URL],
|
||||
"http://127.0.0.1/traced/",
|
||||
)
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/"
|
||||
)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
|
||||
|
||||
async def test_not_recording(self):
|
||||
mock_tracer = Mock()
|
||||
mock_span = Mock()
|
||||
mock_span.is_recording.return_value = False
|
||||
mock_tracer.start_span.return_value = mock_span
|
||||
with patch("opentelemetry.trace.get_tracer") as tracer:
|
||||
tracer.return_value = mock_tracer
|
||||
await self.async_client.get("/traced/")
|
||||
self.assertFalse(mock_span.is_recording())
|
||||
self.assertTrue(mock_span.is_recording.called)
|
||||
self.assertFalse(mock_span.set_attribute.called)
|
||||
self.assertFalse(mock_span.set_status.called)
|
||||
|
||||
async def test_traced_post(self):
|
||||
await self.async_client.post("/traced/")
|
||||
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^traced/")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.UNSET)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "POST")
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_URL],
|
||||
"http://127.0.0.1/traced/",
|
||||
)
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/"
|
||||
)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
|
||||
|
||||
async def test_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
await self.async_client.get("/error/")
|
||||
|
||||
spans = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(span.name, "^error/")
|
||||
self.assertEqual(span.kind, SpanKind.SERVER)
|
||||
self.assertEqual(span.status.status_code, StatusCode.ERROR)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
|
||||
self.assertEqual(
|
||||
span.attributes[SpanAttributes.HTTP_URL],
|
||||
"http://127.0.0.1/error/",
|
||||
)
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_ROUTE], "^error/")
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
|
||||
self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 500)
|
||||
|
||||
self.assertEqual(len(span.events), 1)
|
||||
event = span.events[0]
|
||||
self.assertEqual(event.name, "exception")
|
||||
self.assertEqual(
|
||||
event.attributes[SpanAttributes.EXCEPTION_TYPE], "ValueError"
|
||||
)
|
||||
self.assertEqual(
|
||||
event.attributes[SpanAttributes.EXCEPTION_MESSAGE], "error"
|
||||
)
|
||||
|
||||
async def test_exclude_lists(self):
|
||||
await self.async_client.get("/excluded_arg/123")
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 0)
|
||||
|
||||
await self.async_client.get("/excluded_arg/125")
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
await self.async_client.get("/excluded_noarg/")
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
await self.async_client.get("/excluded_noarg2/")
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
async def test_span_name(self):
|
||||
# test no query_string
|
||||
await self.async_client.get("/span_name/1234/")
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
span = span_list[0]
|
||||
self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
|
||||
|
||||
async def test_span_name_for_query_string(self):
|
||||
"""
|
||||
request not have query string
|
||||
"""
|
||||
await self.async_client.get("/span_name/1234/?query=test")
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
span = span_list[0]
|
||||
self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
|
||||
|
||||
async def test_span_name_404(self):
|
||||
await self.async_client.get("/span_name/1234567890/")
|
||||
span_list = self.memory_exporter.get_finished_spans()
|
||||
self.assertEqual(len(span_list), 1)
|
||||
|
||||
span = span_list[0]
|
||||
self.assertEqual(span.name, "HTTP GET")
|
||||
|
||||
async def test_traced_request_attrs(self):
|
||||
await self.async_client.get("/span_name/1234/", CONTENT_TYPE="test/ct")
|
||||
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(span.attributes["content_type"], "test/ct")
|
||||
self.assertNotIn("non_existing_variable", span.attributes)
|
||||
|
||||
async 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 = await self.async_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)
|
||||
|
||||
async def test_trace_response_headers(self):
|
||||
response = await self.async_client.get("/span_name/1234/")
|
||||
|
||||
self.assertNotIn("Server-Timing", response.headers)
|
||||
self.memory_exporter.clear()
|
||||
|
||||
set_global_response_propagator(TraceResponsePropagator())
|
||||
|
||||
response = await self.async_client.get("/span_name/1234/")
|
||||
span = self.memory_exporter.get_finished_spans()[0]
|
||||
|
||||
self.assertIn("traceresponse", response.headers)
|
||||
self.assertEqual(
|
||||
response.headers["Access-Control-Expose-Headers"], "traceresponse",
|
||||
)
|
||||
self.assertEqual(
|
||||
response.headers["traceresponse"],
|
||||
"00-{0}-{1}-01".format(
|
||||
format_trace_id(span.get_span_context().trace_id),
|
||||
format_span_id(span.get_span_context().span_id),
|
||||
),
|
||||
)
|
||||
self.memory_exporter.clear()
|
||||
|
||||
|
||||
class TestMiddlewareAsgiWithTracerProvider(SimpleTestCase, TestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
conf.settings.configure(ROOT_URLCONF=modules[__name__])
|
||||
super().setUpClass()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
setup_test_environment()
|
||||
resource = resources.Resource.create(
|
||||
{"resource-key": "resource-value"}
|
||||
)
|
||||
result = self.create_tracer_provider(resource=resource)
|
||||
tracer_provider, exporter = result
|
||||
self.exporter = exporter
|
||||
_django_instrumentor.instrument(tracer_provider=tracer_provider)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
teardown_test_environment()
|
||||
_django_instrumentor.uninstrument()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
conf.settings = conf.LazySettings()
|
||||
|
||||
async def test_tracer_provider_traced(self):
|
||||
await self.async_client.post("/traced/")
|
||||
|
||||
spans = self.exporter.get_finished_spans()
|
||||
self.assertEqual(len(spans), 1)
|
||||
|
||||
span = spans[0]
|
||||
|
||||
self.assertEqual(
|
||||
span.resource.attributes["resource-key"], "resource-value"
|
||||
)
|
@ -29,3 +29,35 @@ def route_span_name(
|
||||
request, *args, **kwargs
|
||||
): # pylint: disable=unused-argument
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
async def async_traced(request): # pylint: disable=unused-argument
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
async def async_traced_template(
|
||||
request, year
|
||||
): # pylint: disable=unused-argument
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
async def async_error(request): # pylint: disable=unused-argument
|
||||
raise ValueError("error")
|
||||
|
||||
|
||||
async def async_excluded(request): # pylint: disable=unused-argument
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
async def async_excluded_noarg(request): # pylint: disable=unused-argument
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
async def async_excluded_noarg2(request): # pylint: disable=unused-argument
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
async def async_route_span_name(
|
||||
request, *args, **kwargs
|
||||
): # pylint: disable=unused-argument
|
||||
return HttpResponse()
|
||||
|
2
tox.ini
2
tox.ini
@ -258,7 +258,7 @@ commands_pre =
|
||||
|
||||
falcon{2,3},flask,django,pyramid,tornado,starlette,fastapi,aiohttp,asgi,requests,urllib,urllib3,wsgi: pip install {toxinidir}/util/opentelemetry-util-http[test]
|
||||
wsgi,falcon{2,3},flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
|
||||
asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
|
||||
asgi,django,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
|
||||
|
||||
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
|
||||
|
||||
|
Reference in New Issue
Block a user