mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-30 13:43:03 +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))
|
([#706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/706))
|
||||||
- `opentelemetry-instrumentation-requests` added exclude urls functionality
|
- `opentelemetry-instrumentation-requests` added exclude urls functionality
|
||||||
([#714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/714))
|
([#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
|
### Changed
|
||||||
- `opentelemetry-instrumentation-botocore` Make common span attributes compliant with semantic conventions
|
- `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
|
### Added
|
||||||
- `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package
|
- `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package
|
||||||
([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586))
|
([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586))
|
||||||
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`,
|
- `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-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))
|
([#576](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/576))
|
||||||
- `opentelemetry-instrumentation-pika` added RabbitMQ's pika module instrumentation.
|
- `opentelemetry-instrumentation-pika` added RabbitMQ's pika module instrumentation.
|
||||||
([#680](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/680))
|
([#680](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/680))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `opentelemetry-instrumentation-fastapi` Allow instrumentation of newer FastAPI versions.
|
- `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."""
|
"""Returns (host, port, full_url) tuple."""
|
||||||
server = scope.get("server") or ["0.0.0.0", 80]
|
server = scope.get("server") or ["0.0.0.0", 80]
|
||||||
port = server[1]
|
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", "")
|
full_path = scope.get("root_path", "") + scope.get("path", "")
|
||||||
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
|
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
|
||||||
return server_host, port, http_url
|
return server_host, port, http_url
|
||||||
|
@ -45,6 +45,8 @@ install_requires =
|
|||||||
opentelemetry-semantic-conventions == 0.24b0
|
opentelemetry-semantic-conventions == 0.24b0
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
|
asgi =
|
||||||
|
opentelemetry-instrumentation-asgi == 0.24b0
|
||||||
test =
|
test =
|
||||||
opentelemetry-test == 0.24b0
|
opentelemetry-test == 0.24b0
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# 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 types
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@ -24,11 +25,11 @@ from opentelemetry.instrumentation.propagators import (
|
|||||||
get_global_response_propagator,
|
get_global_response_propagator,
|
||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.utils import extract_attributes_from_object
|
from opentelemetry.instrumentation.utils import extract_attributes_from_object
|
||||||
|
from opentelemetry.instrumentation.wsgi import add_response_attributes
|
||||||
from opentelemetry.instrumentation.wsgi import (
|
from opentelemetry.instrumentation.wsgi import (
|
||||||
add_response_attributes,
|
collect_request_attributes as wsgi_collect_request_attributes,
|
||||||
collect_request_attributes,
|
|
||||||
wsgi_getter,
|
|
||||||
)
|
)
|
||||||
|
from opentelemetry.instrumentation.wsgi import wsgi_getter
|
||||||
from opentelemetry.propagate import extract
|
from opentelemetry.propagate import extract
|
||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.trace import SpanAttributes
|
||||||
from opentelemetry.trace import Span, SpanKind, use_span
|
from opentelemetry.trace import Span, SpanKind, use_span
|
||||||
@ -43,6 +44,7 @@ except ImportError:
|
|||||||
from django.urls import Resolver404, resolve
|
from django.urls import Resolver404, resolve
|
||||||
|
|
||||||
DJANGO_2_0 = django_version >= (2, 0)
|
DJANGO_2_0 = django_version >= (2, 0)
|
||||||
|
DJANGO_3_0 = django_version >= (3, 0)
|
||||||
|
|
||||||
if DJANGO_2_0:
|
if DJANGO_2_0:
|
||||||
# Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style
|
# Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style
|
||||||
@ -67,6 +69,26 @@ else:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
MiddlewareMixin = object
|
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__)
|
_logger = getLogger(__name__)
|
||||||
_attributes_by_preference = [
|
_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):
|
class _DjangoMiddleware(MiddlewareMixin):
|
||||||
"""Django Middleware for OpenTelemetry"""
|
"""Django Middleware for OpenTelemetry"""
|
||||||
|
|
||||||
@ -140,12 +166,25 @@ 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
|
||||||
|
|
||||||
|
is_asgi_request = _is_asgi_request(request)
|
||||||
|
if not _is_asgi_supported and is_asgi_request:
|
||||||
|
return
|
||||||
|
|
||||||
# pylint:disable=W0212
|
# pylint:disable=W0212
|
||||||
request._otel_start_time = time()
|
request._otel_start_time = time()
|
||||||
|
|
||||||
request_meta = request.META
|
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(
|
span = self._tracer.start_span(
|
||||||
self._get_span_name(request),
|
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():
|
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
|
||||||
)
|
)
|
||||||
|
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():
|
for key, value in attributes.items():
|
||||||
span.set_attribute(key, value)
|
span.set_attribute(key, value)
|
||||||
|
|
||||||
@ -207,15 +259,22 @@ 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
|
||||||
|
|
||||||
|
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)
|
activation = request.META.pop(self._environ_activation_key, None)
|
||||||
span = request.META.pop(self._environ_span_key, None)
|
span = request.META.pop(self._environ_span_key, None)
|
||||||
|
|
||||||
if activation and span:
|
if activation and span:
|
||||||
add_response_attributes(
|
if is_asgi_request:
|
||||||
span,
|
set_status_code(span, response.status_code)
|
||||||
f"{response.status_code} {response.reason_phrase}",
|
else:
|
||||||
response,
|
add_response_attributes(
|
||||||
)
|
span,
|
||||||
|
f"{response.status_code} {response.reason_phrase}",
|
||||||
|
response,
|
||||||
|
)
|
||||||
|
|
||||||
propagator = get_global_response_propagator()
|
propagator = get_global_response_propagator()
|
||||||
if propagator:
|
if propagator:
|
||||||
@ -238,7 +297,7 @@ class _DjangoMiddleware(MiddlewareMixin):
|
|||||||
activation.__exit__(None, None, None)
|
activation.__exit__(None, None, None)
|
||||||
|
|
||||||
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.META.get(self._environ_token))
|
||||||
request.META.pop(self._environ_token)
|
request.META.pop(self._environ_token)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -15,12 +15,11 @@
|
|||||||
from sys import modules
|
from sys import modules
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from django import VERSION
|
from django import VERSION, conf
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls import url
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
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.test.utils import setup_test_environment, teardown_test_environment
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
from opentelemetry.instrumentation.django import (
|
from opentelemetry.instrumentation.django import (
|
||||||
DjangoInstrumentor,
|
DjangoInstrumentor,
|
||||||
@ -57,13 +56,13 @@ from .views import (
|
|||||||
DJANGO_2_2 = VERSION >= (2, 2)
|
DJANGO_2_2 = VERSION >= (2, 2)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r"^traced/", traced),
|
re_path(r"^traced/", traced),
|
||||||
url(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
|
re_path(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
|
||||||
url(r"^error/", error),
|
re_path(r"^error/", error),
|
||||||
url(r"^excluded_arg/", excluded),
|
re_path(r"^excluded_arg/", excluded),
|
||||||
url(r"^excluded_noarg/", excluded_noarg),
|
re_path(r"^excluded_noarg/", excluded_noarg),
|
||||||
url(r"^excluded_noarg2/", excluded_noarg2),
|
re_path(r"^excluded_noarg2/", excluded_noarg2),
|
||||||
url(r"^span_name/([0-9]{4})/$", route_span_name),
|
re_path(r"^span_name/([0-9]{4})/$", route_span_name),
|
||||||
]
|
]
|
||||||
_django_instrumentor = DjangoInstrumentor()
|
_django_instrumentor = DjangoInstrumentor()
|
||||||
|
|
||||||
@ -71,8 +70,8 @@ _django_instrumentor = DjangoInstrumentor()
|
|||||||
class TestMiddleware(TestBase, WsgiTestBase):
|
class TestMiddleware(TestBase, WsgiTestBase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
conf.settings.configure(ROOT_URLCONF=modules[__name__])
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
settings.configure(ROOT_URLCONF=modules[__name__])
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@ -105,6 +104,11 @@ class TestMiddleware(TestBase, WsgiTestBase):
|
|||||||
teardown_test_environment()
|
teardown_test_environment()
|
||||||
_django_instrumentor.uninstrument()
|
_django_instrumentor.uninstrument()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
conf.settings = conf.LazySettings()
|
||||||
|
|
||||||
def test_templated_route_get(self):
|
def test_templated_route_get(self):
|
||||||
Client().get("/route/2020/template/")
|
Client().get("/route/2020/template/")
|
||||||
|
|
||||||
@ -357,6 +361,7 @@ class TestMiddleware(TestBase, WsgiTestBase):
|
|||||||
class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase):
|
class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
conf.settings.configure(ROOT_URLCONF=modules[__name__])
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -375,6 +380,11 @@ class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase):
|
|||||||
teardown_test_environment()
|
teardown_test_environment()
|
||||||
_django_instrumentor.uninstrument()
|
_django_instrumentor.uninstrument()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
conf.settings = conf.LazySettings()
|
||||||
|
|
||||||
def test_tracer_provider_traced(self):
|
def test_tracer_provider_traced(self):
|
||||||
Client().post("/traced/")
|
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
|
request, *args, **kwargs
|
||||||
): # pylint: disable=unused-argument
|
): # pylint: disable=unused-argument
|
||||||
return HttpResponse()
|
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]
|
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]
|
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]
|
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user