From 27e4c2ce34156bd0dd967cafb79c91aa1e85f498 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 3 Apr 2024 15:20:57 -0700 Subject: [PATCH 01/19] Update .pylintrc --- .pylintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintrc b/.pylintrc index 5ea4385ea0..114dadef75 100644 --- a/.pylintrc +++ b/.pylintrc @@ -81,6 +81,7 @@ disable=missing-docstring, missing-module-docstring, # temp-pylint-upgrade import-error, # needed as a workaround as reported here: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/290 cyclic-import, + not-context-manager # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option From 45a8808f714265c07377a02622ad7d8c1d919ca8 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Tue, 16 Jul 2024 14:14:32 -0700 Subject: [PATCH 02/19] django --- CHANGELOG.md | 2 + .../aiohttp_server/__init__.py | 2 +- .../instrumentation/django/__init__.py | 49 +++++-- .../django/middleware/otel_middleware.py | 134 ++++++++++++------ .../instrumentation/falcon/__init__.py | 2 +- .../instrumentation/pyramid/callbacks.py | 2 +- .../instrumentation/tornado/__init__.py | 2 +- 7 files changed, 134 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57447ccf97..62bc650edf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2673](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2673)) - `opentelemetry-instrumentation-django` Add `http.target` to Django duration metric attributes ([#2624](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2624)) + - `opentelemetry-instrumentation-django` Implement new semantic convention opt-in with stable http semantic conventions + ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) ### Breaking changes diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py index 2e519ac1c5..659ff24af6 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py @@ -207,7 +207,7 @@ async def middleware(request, handler): duration_histogram = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="Duration of HTTP client requests.", + description="Duration of HTTP server requests.", ) active_requests_counter = meter.create_up_down_counter( diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index 37ac760283..cdeafb2d23 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -243,6 +243,13 @@ def response_hook(span, request, response): from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from opentelemetry.instrumentation._semconv import ( + _get_schema_url, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, +) from opentelemetry.instrumentation.django.environment_variables import ( OTEL_PYTHON_DJANGO_INSTRUMENT, ) @@ -253,7 +260,13 @@ def response_hook(span, request, response): from opentelemetry.instrumentation.django.version import __version__ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.metrics import get_meter +from opentelemetry.semconv._incubating.metrics.http_metrics import ( + create_http_server_active_requests, +) from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_SERVER_REQUEST_DURATION, +) from opentelemetry.trace import get_tracer from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls @@ -293,6 +306,12 @@ def _instrument(self, **kwargs): if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False": return + # initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + tracer_provider = kwargs.get("tracer_provider") meter_provider = kwargs.get("meter_provider") _excluded_urls = kwargs.get("excluded_urls") @@ -300,14 +319,15 @@ def _instrument(self, **kwargs): __name__, __version__, tracer_provider=tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) meter = get_meter( __name__, __version__, meter_provider=meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url(sem_conv_opt_in_mode), ) + _DjangoMiddleware._sem_conv_opt_in_mode = sem_conv_opt_in_mode _DjangoMiddleware._tracer = tracer _DjangoMiddleware._meter = meter _DjangoMiddleware._excluded_urls = ( @@ -319,15 +339,22 @@ def _instrument(self, **kwargs): _DjangoMiddleware._otel_response_hook = kwargs.pop( "response_hook", None ) - _DjangoMiddleware._duration_histogram = meter.create_histogram( - name=MetricInstruments.HTTP_SERVER_DURATION, - unit="ms", - description="Duration of HTTP client requests.", - ) - _DjangoMiddleware._active_request_counter = meter.create_up_down_counter( - name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, - unit="requests", - description="measures the number of concurrent HTTP requests those are currently in flight", + _DjangoMiddleware._duration_histogram_old = None + if _report_old(sem_conv_opt_in_mode): + _DjangoMiddleware._duration_histogram_old = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="Duration of HTTP server requests.", + ) + _DjangoMiddleware._duration_histogram_new = None + if _report_new(sem_conv_opt_in_mode): + self.duration_histogram_new = self.meter.create_histogram( + name=HTTP_SERVER_REQUEST_DURATION, + description="Duration of HTTP server requests.", + unit="s", + ) + _DjangoMiddleware._active_request_counter = create_http_server_active_requests( + meter ) # This can not be solved, but is an inherent problem of this approach: # the order of middleware entries matters, and here you have no control diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index 11e92c9757..26907a2b9a 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -22,6 +22,31 @@ from django.http import HttpRequest, HttpResponse from opentelemetry.context import detach +from opentelemetry.instrumentation._semconv import ( + _filter_semconv_active_request_count_attr, + _filter_semconv_duration_attrs, + _get_schema_url, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, + _set_http_flavor_version, + _set_http_host, + _set_http_method, + _set_http_net_host_port, + _set_http_peer_ip, + _set_http_peer_port_server, + _set_http_scheme, + _set_http_target, + _set_http_url, + _set_http_user_agent, + _set_status, +) from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) @@ -40,6 +65,9 @@ collect_request_attributes as wsgi_collect_request_attributes, ) from opentelemetry.instrumentation.wsgi import wsgi_getter +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_ROUTE, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import ( @@ -47,8 +75,6 @@ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, - _parse_active_request_count_attrs, - _parse_duration_attrs, get_custom_headers, get_excluded_urls, get_traced_request_attrs, @@ -113,26 +139,6 @@ def __call__(self, request): _is_asgi_supported = False _logger = getLogger(__name__) -_attributes_by_preference = [ - [ - SpanAttributes.HTTP_SCHEME, - SpanAttributes.HTTP_HOST, - SpanAttributes.HTTP_TARGET, - ], - [ - SpanAttributes.HTTP_SCHEME, - SpanAttributes.HTTP_SERVER_NAME, - SpanAttributes.NET_HOST_PORT, - SpanAttributes.HTTP_TARGET, - ], - [ - SpanAttributes.HTTP_SCHEME, - SpanAttributes.NET_HOST_NAME, - SpanAttributes.NET_HOST_PORT, - SpanAttributes.HTTP_TARGET, - ], - [SpanAttributes.HTTP_URL], -] def _is_asgi_request(request: HttpRequest) -> bool: @@ -159,8 +165,10 @@ class _DjangoMiddleware(MiddlewareMixin): _excluded_urls = get_excluded_urls("DJANGO") _tracer = None _meter = None - _duration_histogram = None + _duration_histogram_old = None + _duration_histogram_new = None _active_request_counter = None + _sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT _otel_request_hook: Callable[[Span, HttpRequest], None] = None _otel_response_hook: Callable[[Span, HttpRequest, HttpResponse], None] = ( @@ -226,14 +234,15 @@ def process_request(self, request): ) active_requests_count_attrs = _parse_active_request_count_attrs( - attributes + attributes, + self._sem_conv_opt_in_mode, ) - duration_attrs = _parse_duration_attrs(attributes) request.META[self._environ_active_request_attr_key] = ( active_requests_count_attrs ) - request.META[self._environ_duration_attr_key] = duration_attrs + # Pass all of attributes to duration key because we will filter during response + request.META[self._environ_duration_attr_key] = attributes self._active_request_counter.add(1, active_requests_count_attrs) if span.is_recording(): attributes = extract_attributes_from_object( @@ -309,18 +318,20 @@ def process_view(self, request, view_func, *args, **kwargs): ): span = request.META[self._environ_span_key] - if span.is_recording(): - match = getattr(request, "resolver_match", None) - if match: - route = getattr(match, "route", None) - if route: + match = getattr(request, "resolver_match", None) + if match: + route = getattr(match, "route", None) + if route: + if span.is_recording(): + # http.route is present for both old and new semconv span.set_attribute(SpanAttributes.HTTP_ROUTE, route) - duration_attrs = request.META[ - self._environ_duration_attr_key - ] - # Metrics currently use the 1.11.0 schema, which puts the route in `http.target`. - # TODO: use `http.route` when the user sets `OTEL_SEMCONV_STABILITY_OPT_IN`. + duration_attrs = request.META[ + self._environ_duration_attr_key + ] + if _report_old(self._sem_conv_opt_in_mode): duration_attrs[SpanAttributes.HTTP_TARGET] = route + if _report_new(self._sem_conv_opt_in_mode): + duration_attrs[HTTP_ROUTE] = route def process_exception(self, request, exception): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): @@ -347,15 +358,16 @@ def process_response(self, request, response): duration_attrs = request.META.pop( self._environ_duration_attr_key, None ) - if duration_attrs: - duration_attrs[SpanAttributes.HTTP_STATUS_CODE] = ( - response.status_code - ) request_start_time = request.META.pop(self._environ_timer_key, None) if activation and span: if is_asgi_request: - set_status_code(span, response.status_code) + set_status_code( + span, + response.status_code, + metric_attributes=duration_attrs, + sem_conv_opt_in_mode=self._sem_conv_opt_in_mode, + ) if span.is_recording() and span.kind == SpanKind.SERVER: custom_headers = {} @@ -381,6 +393,8 @@ def process_response(self, request, response): span, f"{response.status_code} {response.reason_phrase}", response.items(), + duration_attrs=duration_attrs, + sem_conv_opt_in_mode=self._sem_conv_opt_in_mode, ) if span.is_recording() and span.kind == SpanKind.SERVER: custom_attributes = ( @@ -416,13 +430,45 @@ def process_response(self, request, response): activation.__exit__(None, None, None) if request_start_time is not None: - duration = max( - round((default_timer() - request_start_time) * 1000), 0 + duration_attrs_old = _parse_duration_attrs( + duration_attrs, _HTTPStabilityMode.DEFAULT + ) + duration_attrs_new = _parse_duration_attrs( + duration_attrs, _HTTPStabilityMode.HTTP ) - self._duration_histogram.record(duration, duration_attrs) + duration_s = default_timer() - request_start_time + if self._duration_histogram_old: + self._duration_histogram_old.record( + max(round(duration_s * 1000), 0), duration_attrs_old + ) + if self._duration_histogram_new: + self._duration_histogram_new.record( + max(duration_s, 0), duration_attrs_new + ) self._active_request_counter.add(-1, active_requests_count_attrs) if request.META.get(self._environ_token, None) is not None: detach(request.META.get(self._environ_token)) request.META.pop(self._environ_token) return response + +def _parse_duration_attrs( + req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT +): + return _filter_semconv_duration_attrs( + req_attrs, + _server_duration_attrs_old, + _server_duration_attrs_new, + sem_conv_opt_in_mode, + ) + + +def _parse_active_request_count_attrs( + req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT +): + return _filter_semconv_active_request_count_attr( + req_attrs, + _server_active_requests_count_attrs_old, + _server_active_requests_count_attrs_new, + sem_conv_opt_in_mode, + ) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index 79c9a0cf0f..1a252b9a16 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -268,7 +268,7 @@ def __init__(self, *args, **kwargs): self.duration_histogram = self._otel_meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="Duration of HTTP client requests.", + description="Duration of HTTP server requests.", ) self.active_requests_counter = self._otel_meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, diff --git a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py index d0010ed8d0..09f1645384 100644 --- a/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py +++ b/instrumentation/opentelemetry-instrumentation-pyramid/src/opentelemetry/instrumentation/pyramid/callbacks.py @@ -141,7 +141,7 @@ def trace_tween_factory(handler, registry): duration_histogram = meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="Duration of HTTP client requests.", + description="Duration of HTTP server requests.", ) active_requests_counter = meter.create_up_down_counter( name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS, diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index be9129bda0..1b56db3876 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -296,7 +296,7 @@ def _create_server_histograms(meter) -> Dict[str, Histogram]: MetricInstruments.HTTP_SERVER_DURATION: meter.create_histogram( name=MetricInstruments.HTTP_SERVER_DURATION, unit="ms", - description="Duration of HTTP client requests.", + description="Duration of HTTP server requests.", ), MetricInstruments.HTTP_SERVER_REQUEST_SIZE: meter.create_histogram( name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE, From cd714f9f0cbfac57df6959ec895da64feccb7359 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 18 Jul 2024 13:39:18 -0700 Subject: [PATCH 03/19] wsgi --- CHANGELOG.md | 4 +- .../instrumentation/django/__init__.py | 2 +- .../django/middleware/otel_middleware.py | 13 +- .../tests/test_middleware.py | 1345 ++++++++++------- 4 files changed, 851 insertions(+), 513 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 490cd7f1cd..46e5a041c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `asgi` middleware ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) - - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` middleware + - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` instrumentation + ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) + - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) ### Fixed diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index cdeafb2d23..e298d2dbb1 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -348,7 +348,7 @@ def _instrument(self, **kwargs): ) _DjangoMiddleware._duration_histogram_new = None if _report_new(sem_conv_opt_in_mode): - self.duration_histogram_new = self.meter.create_histogram( + _DjangoMiddleware._duration_histogram_new = meter.create_histogram( name=HTTP_SERVER_REQUEST_DURATION, description="Duration of HTTP server requests.", unit="s", diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index 26907a2b9a..89ce07b972 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -80,6 +80,7 @@ get_traced_request_attrs, normalise_request_header_name, normalise_response_header_name, + sanitize_method, ) try: @@ -177,6 +178,9 @@ class _DjangoMiddleware(MiddlewareMixin): @staticmethod def _get_span_name(request): + method = sanitize_method(request.method.strip()) + if method == "_OTHER": + return "HTTP" try: if getattr(request, "resolver_match"): match = request.resolver_match @@ -184,10 +188,10 @@ def _get_span_name(request): match = resolve(request.path) if hasattr(match, "route") and match.route: - return f"{request.method} {match.route}" + return f"{method} {match.route}" if hasattr(match, "url_name") and match.url_name: - return f"{request.method} {match.url_name}" + return f"{method} {match.url_name}" return request.method @@ -221,7 +225,10 @@ def process_request(self, request): carrier_getter = wsgi_getter collect_request_attributes = wsgi_collect_request_attributes - attributes = collect_request_attributes(carrier) + attributes = collect_request_attributes( + carrier, + self._sem_conv_opt_in_mode, + ) span, token = _start_internal_or_server_span( tracer=self._tracer, span_name=self._get_span_name(request), diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 4945f05455..44c41a8f89 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -24,6 +24,16 @@ from django.test.utils import setup_test_environment, teardown_test_environment from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetryStabilitySignalType, + _HTTPStabilityMode, + _OpenTelemetrySemanticConventionStability, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, +) from opentelemetry.instrumentation.django import ( DjangoInstrumentor, _DjangoMiddleware, @@ -39,6 +49,35 @@ ) from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.semconv.attributes.client_attributes import ( + CLIENT_ADDRESS, + CLIENT_PORT, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + HTTP_ROUTE, +) +from opentelemetry.semconv.attributes.exception_attributes import ( + EXCEPTION_MESSAGE, + EXCEPTION_TYPE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import ( + URL_FULL, + URL_PATH, + URL_QUERY, + URL_SCHEME, +) +from opentelemetry.semconv.attributes.user_agent_attributes import ( + USER_AGENT_ORIGINAL, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import ( @@ -51,8 +90,6 @@ OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, - _active_requests_count_attrs, - _duration_attrs, get_excluded_urls, get_traced_request_attrs, ) @@ -112,15 +149,25 @@ def setUpClass(cls): def setUp(self): super().setUp() setup_test_environment() - _django_instrumentor.instrument() + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" 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", + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) + _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() + _django_instrumentor.instrument() self.exclude_patch = patch( "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls", get_excluded_urls("DJANGO"), @@ -145,332 +192,517 @@ def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() - def test_templated_route_get(self): - Client().get("/route/2020/template/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual( - span.name, - ( - "GET ^route/(?P[0-9]{4})/template/$" - if DJANGO_2_2 - else "GET" - ), - ) - 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://testserver/route/2020/template/", - ) - if DJANGO_2_2: - self.assertEqual( - span.attributes[SpanAttributes.HTTP_ROUTE], - "^route/(?P[0-9]{4})/template/$", - ) - self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") - self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) - - def test_traced_get(self): - Client().get("/traced/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") - 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://testserver/traced/", - ) - if DJANGO_2_2: - 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) - - 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 - 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) - - def test_empty_path(self): - Client().get("/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual(span.name, "GET empty") - - def test_traced_post(self): - Client().post("/traced/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") - 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://testserver/traced/", - ) - if DJANGO_2_2: - 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) - - def test_error(self): - with self.assertRaises(ValueError): - Client().get("/error/") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - - self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") - 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://testserver/error/", - ) - if DJANGO_2_2: - 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" - ) - - def test_exclude_lists(self): - client = Client() - client.get("/excluded_arg/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - - client.get("/excluded_arg/125") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - client.get("/excluded_noarg/") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - client.get("/excluded_noarg2/") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - def test_exclude_lists_through_instrument(self): - _django_instrumentor.uninstrument() - _django_instrumentor.instrument(excluded_urls="excluded_explicit") - client = Client() - client.get("/excluded_explicit") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) - - client.get("/excluded_arg/123") - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - - def test_span_name(self): - # test no query_string - 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, - "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", - ) - - def test_span_name_for_query_string(self): - """ - request not have query string - """ - Client().get("/span_name/1234/?query=test") + # def test_templated_route_get(self): + # Client().get("/route/2020/template/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual( + # span.name, + # ( + # "GET ^route/(?P[0-9]{4})/template/$" + # if DJANGO_2_2 + # else "GET" + # ), + # ) + # 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://testserver/route/2020/template/", + # ) + # if DJANGO_2_2: + # self.assertEqual( + # span.attributes[SpanAttributes.HTTP_ROUTE], + # "^route/(?P[0-9]{4})/template/$", + # ) + # self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + # self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + + # def test_traced_get(self): + # Client().get("/traced/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") + # 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://testserver/traced/", + # ) + # if DJANGO_2_2: + # 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) + + # def test_traced_get_new_semconv(self): + # Client().get("/traced/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") + # self.assertEqual(span.kind, SpanKind.SERVER) + # self.assertEqual(span.status.status_code, StatusCode.UNSET) + # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + # self.assertEqual(span.attributes[URL_SCHEME], "http") + # self.assertEqual(span.attributes[SERVER_PORT], 80) + # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + # if DJANGO_2_2: + # self.assertEqual( + # span.attributes[HTTP_ROUTE], "^traced/" + # ) + # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + # def test_traced_get_both_semconv(self): + # Client().get("/traced/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") + # 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://testserver/traced/", + # ) + # if DJANGO_2_2: + # 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) + # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + # self.assertEqual(span.attributes[URL_SCHEME], "http") + # self.assertEqual(span.attributes[SERVER_PORT], 80) + # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + # if DJANGO_2_2: + # self.assertEqual( + # span.attributes[HTTP_ROUTE], "^traced/" + # ) + # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + +# 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 +# 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) + +# def test_empty_path(self): +# Client().get("/") + +# spans = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(spans), 1) + +# span = spans[0] + +# self.assertEqual(span.name, "GET empty") + + # def test_traced_post(self): + # Client().post("/traced/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") + # 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://testserver/traced/", + # ) + # if DJANGO_2_2: + # 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) + + # def test_traced_post_new_semconv(self): + # Client().post("/traced/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") + # self.assertEqual(span.kind, SpanKind.SERVER) + # self.assertEqual(span.status.status_code, StatusCode.UNSET) + # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") + # self.assertEqual(span.attributes[URL_SCHEME], "http") + # self.assertEqual(span.attributes[SERVER_PORT], 80) + # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + # if DJANGO_2_2: + # self.assertEqual( + # span.attributes[HTTP_ROUTE], "^traced/" + # ) + # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + # def test_traced_post_both_semconv(self): + # Client().post("/traced/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") + # 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://testserver/traced/", + # ) + # self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + # self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") + # self.assertEqual(span.attributes[URL_SCHEME], "http") + # self.assertEqual(span.attributes[SERVER_PORT], 80) + # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + # if DJANGO_2_2: + # self.assertEqual( + # span.attributes[HTTP_ROUTE], "^traced/" + # ) + # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + # def test_error(self): + # with self.assertRaises(ValueError): + # Client().get("/error/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") + # 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://testserver/error/", + # ) + # if DJANGO_2_2: + # 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" + # ) + + # def test_error_new_semconv(self): + # with self.assertRaises(ValueError): + # Client().get("/error/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") + # self.assertEqual(span.kind, SpanKind.SERVER) + # self.assertEqual(span.status.status_code, StatusCode.ERROR) + # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + # if DJANGO_2_2: + # self.assertEqual( + # span.attributes[HTTP_ROUTE], "^error/" + # ) + # self.assertEqual(span.attributes[URL_SCHEME], "http") + # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) + + # self.assertEqual(len(span.events), 1) + # event = span.events[0] + # self.assertEqual(event.name, "exception") + # self.assertEqual( + # event.attributes[EXCEPTION_TYPE], "ValueError" + # ) + # self.assertEqual( + # event.attributes[EXCEPTION_MESSAGE], "error" + # ) + + # def test_error_both_semconv(self): + # with self.assertRaises(ValueError): + # Client().get("/error/") + + # spans = self.memory_exporter.get_finished_spans() + # self.assertEqual(len(spans), 1) + + # span = spans[0] + + # self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") + # 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://testserver/error/", + # ) + # if DJANGO_2_2: + # 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(span.attributes[HTTP_REQUEST_METHOD], "GET") + # if DJANGO_2_2: + # self.assertEqual( + # span.attributes[HTTP_ROUTE], "^error/" + # ) + # self.assertEqual(span.attributes[URL_SCHEME], "http") + # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) + + # self.assertEqual(len(span.events), 1) + # event = span.events[0] + # self.assertEqual(event.name, "exception") + # self.assertEqual( + # event.attributes[EXCEPTION_TYPE], "ValueError" + # ) + # self.assertEqual( + # event.attributes[EXCEPTION_MESSAGE], "error" + # ) + +# def test_exclude_lists(self): +# client = Client() +# client.get("/excluded_arg/123") +# span_list = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(span_list), 0) + +# client.get("/excluded_arg/125") +# span_list = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(span_list), 1) + +# client.get("/excluded_noarg/") +# span_list = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(span_list), 1) + +# client.get("/excluded_noarg2/") +# span_list = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(span_list), 1) + +# def test_exclude_lists_through_instrument(self): +# _django_instrumentor.uninstrument() +# _django_instrumentor.instrument(excluded_urls="excluded_explicit") +# client = Client() +# client.get("/excluded_explicit") +# span_list = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(span_list), 0) + +# client.get("/excluded_arg/123") +# span_list = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(span_list), 1) + + # def test_span_name(self): + # # test no query_string + # 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, + # "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", + # ) + + # def test_span_name_for_query_string(self): + # """ + # request not have query string + # """ + # 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, + # "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", + # ) + + # def test_span_name_404(self): + # 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, "GET") + + def test_nonstandard_http_method_span_name(self): + Client().request(REQUEST_METHOD="NONSTANDARD", PATH_INFO="/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, - "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", - ) - - def test_span_name_404(self): - 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, "GET") - - def test_traced_request_attrs(self): - 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) - - 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 = 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) - - def test_request_hook_exception(self): - def request_hook(span, request): - # pylint: disable=broad-exception-raised - raise Exception("request hook exception") - - _DjangoMiddleware._otel_request_hook = request_hook - Client().get("/span_name/1234/") - _DjangoMiddleware._otel_request_hook = None - - # ensure that span ended - finished_spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(finished_spans), 1) - - def test_response_hook_exception(self): - def response_hook(span, request, response): - # pylint: disable=broad-exception-raised - raise Exception("response hook exception") - - _DjangoMiddleware._otel_response_hook = response_hook - Client().get("/span_name/1234/") - _DjangoMiddleware._otel_response_hook = None - - # ensure that span ended - finished_spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(finished_spans), 1) - - def test_trace_parent(self): - id_generator = RandomIdGenerator() - trace_id = format_trace_id(id_generator.generate_trace_id()) - span_id = format_span_id(id_generator.generate_span_id()) - traceparent_value = f"00-{trace_id}-{span_id}-01" - - Client().get( - "/span_name/1234/", - HTTP_TRACEPARENT=traceparent_value, - ) - span = self.memory_exporter.get_finished_spans()[0] - - self.assertEqual( - trace_id, - format_trace_id(span.get_span_context().trace_id), - ) - self.assertIsNotNone(span.parent) - self.assertEqual( - trace_id, - format_trace_id(span.parent.trace_id), - ) - self.assertEqual( - span_id, - format_span_id(span.parent.span_id), - ) - self.memory_exporter.clear() - - def test_trace_response_headers(self): - response = Client().get("/span_name/1234/") - - self.assertFalse(response.has_header("Server-Timing")) - self.memory_exporter.clear() - - set_global_response_propagator(TraceResponsePropagator()) - - response = Client().get("/span_name/1234/") - self.assertTraceResponseHeaderMatchesSpan( - response, - self.memory_exporter.get_finished_spans()[0], - ) - self.memory_exporter.clear() - - def test_uninstrument(self): - Client().get("/route/2020/template/") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - self.memory_exporter.clear() - _django_instrumentor.uninstrument() - - Client().get("/route/2020/template/") - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 0) + self.assertEqual(span.name, "HTTP") + +# def test_traced_request_attrs(self): +# 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) + +# 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 = 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) + +# def test_request_hook_exception(self): +# def request_hook(span, request): +# # pylint: disable=broad-exception-raised +# raise Exception("request hook exception") + +# _DjangoMiddleware._otel_request_hook = request_hook +# Client().get("/span_name/1234/") +# _DjangoMiddleware._otel_request_hook = None + +# # ensure that span ended +# finished_spans = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(finished_spans), 1) + +# def test_response_hook_exception(self): +# def response_hook(span, request, response): +# # pylint: disable=broad-exception-raised +# raise Exception("response hook exception") + +# _DjangoMiddleware._otel_response_hook = response_hook +# Client().get("/span_name/1234/") +# _DjangoMiddleware._otel_response_hook = None + +# # ensure that span ended +# finished_spans = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(finished_spans), 1) + +# def test_trace_parent(self): +# id_generator = RandomIdGenerator() +# trace_id = format_trace_id(id_generator.generate_trace_id()) +# span_id = format_span_id(id_generator.generate_span_id()) +# traceparent_value = f"00-{trace_id}-{span_id}-01" + +# Client().get( +# "/span_name/1234/", +# HTTP_TRACEPARENT=traceparent_value, +# ) +# span = self.memory_exporter.get_finished_spans()[0] + +# self.assertEqual( +# trace_id, +# format_trace_id(span.get_span_context().trace_id), +# ) +# self.assertIsNotNone(span.parent) +# self.assertEqual( +# trace_id, +# format_trace_id(span.parent.trace_id), +# ) +# self.assertEqual( +# span_id, +# format_span_id(span.parent.span_id), +# ) +# self.memory_exporter.clear() + +# def test_trace_response_headers(self): +# response = Client().get("/span_name/1234/") + +# self.assertFalse(response.has_header("Server-Timing")) +# self.memory_exporter.clear() + +# set_global_response_propagator(TraceResponsePropagator()) + +# response = Client().get("/span_name/1234/") +# self.assertTraceResponseHeaderMatchesSpan( +# response, +# self.memory_exporter.get_finished_spans()[0], +# ) +# self.memory_exporter.clear() + +# def test_uninstrument(self): +# Client().get("/route/2020/template/") +# spans = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(spans), 1) + +# self.memory_exporter.clear() +# _django_instrumentor.uninstrument() + +# Client().get("/route/2020/template/") +# spans = self.memory_exporter.get_finished_spans() +# self.assertEqual(len(spans), 0) # pylint: disable=too-many-locals def test_wsgi_metrics(self): @@ -479,9 +711,8 @@ def test_wsgi_metrics(self): "http.server.duration", ] _recommended_attrs = { - "http.server.active_requests": _active_requests_count_attrs, - "http.server.duration": _duration_attrs - | {SpanAttributes.HTTP_TARGET}, + "http.server.active_requests": _server_active_requests_count_attrs_old, + "http.server.duration": _server_duration_attrs_old, } start = default_timer() for _ in range(3): @@ -517,192 +748,290 @@ def test_wsgi_metrics(self): ) self.assertTrue(histrogram_data_point_seen and number_data_point_seen) - def test_wsgi_metrics_unistrument(self): - Client().get("/span_name/1234/") - _django_instrumentor.uninstrument() - Client().get("/span_name/1234/") + # pylint: disable=too-many-locals + def test_wsgi_metrics_new_semconv(self): + _expected_metric_names = [ + "http.server.active_requests", + "http.server.request.duration", + ] + _recommended_attrs = { + "http.server.active_requests": _server_active_requests_count_attrs_new, + "http.server.request.duration": _server_duration_attrs_new, + } + start = default_timer() + for _ in range(3): + response = Client().get("/span_name/1234/") + self.assertEqual(response.status_code, 200) + duration = default_timer() - start metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histrogram_data_point_seen = False + + self.assertTrue(len(metrics_list.resource_metrics) != 0) for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) != 0) for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) != 0) for metric in scope_metric.metrics: - for point in list(metric.data.data_points): + self.assertIn(metric.name, _expected_metric_names) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: if isinstance(point, HistogramDataPoint): - self.assertEqual(1, point.count) + self.assertEqual(point.count, 3) + histrogram_data_point_seen = True + self.assertAlmostEqual( + duration, point.sum, places=2 + ) if isinstance(point, NumberDataPoint): - self.assertEqual(0, point.value) - - -class TestMiddlewareWithTracerProvider(WsgiTestBase): - @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 - self.tracer_provider = tracer_provider - _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() - - def test_tracer_provider_traced(self): - 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" - ) - - def test_django_with_wsgi_instrumented(self): - tracer = self.tracer_provider.get_tracer(__name__) - with tracer.start_as_current_span( - "test", kind=SpanKind.SERVER - ) as parent_span: - Client().get("/span_name/1234/") - span_list = self.exporter.get_finished_spans() - print(span_list) - self.assertEqual( - span_list[0].attributes[SpanAttributes.HTTP_STATUS_CODE], 200 - ) - self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) - self.assertEqual( - parent_span.get_span_context().span_id, - span_list[0].parent.span_id, - ) - - -@patch.dict( - "os.environ", - { - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", - OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", - }, -) -class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase): - @classmethod - def setUpClass(cls): - conf.settings.configure(ROOT_URLCONF=modules[__name__]) - super().setUpClass() - - def setUp(self): - super().setUp() - setup_test_environment() - tracer_provider, exporter = self.create_tracer_provider() - self.exporter = exporter - _django_instrumentor.instrument(tracer_provider=tracer_provider) + number_data_point_seen = True + self.assertEqual(point.value, 0) + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs[metric.name] + ) + self.assertTrue(histrogram_data_point_seen and number_data_point_seen) - def tearDown(self): - super().tearDown() - teardown_test_environment() - _django_instrumentor.uninstrument() + # pylint: disable=too-many-locals + def test_wsgi_metrics_both_semconv(self): + _expected_metric_names = [ + "http.server.duration", + "http.server.active_requests", + "http.server.request.duration", + ] + active_count_both_attrs = list(_server_active_requests_count_attrs_new) + active_count_both_attrs.extend(_server_active_requests_count_attrs_old) + _recommended_attrs = { + "http.server.active_requests": active_count_both_attrs, + "http.server.request.duration": _server_duration_attrs_new, + "http.server.duration": _server_duration_attrs_old, + } + start = default_timer() + for _ in range(3): + response = Client().get("/span_name/1234/") + self.assertEqual(response.status_code, 200) + duration = max(round((default_timer() - start) * 1000), 0) + duration_s = max(default_timer() - start, 0) + metrics_list = self.memory_metrics_reader.get_metrics_data() + number_data_point_seen = False + histrogram_data_point_seen = False - @classmethod - def tearDownClass(cls): - super().tearDownClass() - conf.settings = conf.LazySettings() + self.assertTrue(len(metrics_list.resource_metrics) != 0) + for resource_metric in metrics_list.resource_metrics: + self.assertTrue(len(resource_metric.scope_metrics) != 0) + for scope_metric in resource_metric.scope_metrics: + self.assertTrue(len(scope_metric.metrics) != 0) + for metric in scope_metric.metrics: + self.assertIn(metric.name, _expected_metric_names) + data_points = list(metric.data.data_points) + self.assertEqual(len(data_points), 1) + for point in data_points: + if isinstance(point, HistogramDataPoint): + self.assertEqual(point.count, 3) + histrogram_data_point_seen = True + if metric.name == "http.server.request.duration": + self.assertAlmostEqual( + duration_s, point.sum, places=2 + ) + elif metric.name == "http.server.duration": + self.assertAlmostEqual( + duration, point.sum, delta=5 + ) + if isinstance(point, NumberDataPoint): + number_data_point_seen = True + self.assertEqual(point.value, 0) + for attr in point.attributes: + self.assertIn( + attr, _recommended_attrs[metric.name] + ) + self.assertTrue(histrogram_data_point_seen and number_data_point_seen) - def test_http_custom_request_headers_in_span_attributes(self): - expected = { - "http.request.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.request.header.custom_test_header_2": ( - "test-header-value-2", - ), - "http.request.header.regex_test_header_1": ("Regex Test Value 1",), - "http.request.header.regex_test_header_2": ( - "RegexTestValue2,RegexTestValue3", - ), - "http.request.header.my_secret_header": ("[REDACTED]",), - } - Client( - HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", - HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", - HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1", - HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3", - HTTP_MY_SECRET_HEADER="My Secret Value", - ).get("/traced/") - spans = self.exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertSpanHasAttributes(span, expected) - self.memory_exporter.clear() - - def test_http_custom_request_headers_not_in_span_attributes(self): - not_expected = { - "http.request.header.custom_test_header_2": ( - "test-header-value-2", - ), - } - Client(HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1").get("/traced/") - spans = self.exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - self.assertEqual(span.kind, SpanKind.SERVER) - for key, _ in not_expected.items(): - self.assertNotIn(key, span.attributes) - self.memory_exporter.clear() - - def test_http_custom_response_headers_in_span_attributes(self): - expected = { - "http.response.header.custom_test_header_1": ( - "test-header-value-1", - ), - "http.response.header.custom_test_header_2": ( - "test-header-value-2", - ), - "http.response.header.my_custom_regex_header_1": ( - "my-custom-regex-value-1,my-custom-regex-value-2", - ), - "http.response.header.my_custom_regex_header_2": ( - "my-custom-regex-value-3,my-custom-regex-value-4", - ), - "http.response.header.my_secret_header": ("[REDACTED]",), - } - Client().get("/traced_custom_header/") - spans = self.exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - self.assertEqual(span.kind, SpanKind.SERVER) - self.assertSpanHasAttributes(span, expected) - self.memory_exporter.clear() - - def test_http_custom_response_headers_not_in_span_attributes(self): - not_expected = { - "http.response.header.custom_test_header_3": ( - "test-header-value-3", - ), - } - Client().get("/traced_custom_header/") - spans = self.exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - - span = spans[0] - self.assertEqual(span.kind, SpanKind.SERVER) - for key, _ in not_expected.items(): - self.assertNotIn(key, span.attributes) - self.memory_exporter.clear() +# def test_wsgi_metrics_unistrument(self): +# Client().get("/span_name/1234/") +# _django_instrumentor.uninstrument() +# Client().get("/span_name/1234/") +# metrics_list = self.memory_metrics_reader.get_metrics_data() +# for resource_metric in metrics_list.resource_metrics: +# for scope_metric in resource_metric.scope_metrics: +# for metric in scope_metric.metrics: +# for point in list(metric.data.data_points): +# if isinstance(point, HistogramDataPoint): +# self.assertEqual(1, point.count) +# if isinstance(point, NumberDataPoint): +# self.assertEqual(0, point.value) + + +# class TestMiddlewareWithTracerProvider(WsgiTestBase): +# @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 +# self.tracer_provider = tracer_provider +# _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() + +# def test_tracer_provider_traced(self): +# 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" +# ) + +# def test_django_with_wsgi_instrumented(self): +# tracer = self.tracer_provider.get_tracer(__name__) +# with tracer.start_as_current_span( +# "test", kind=SpanKind.SERVER +# ) as parent_span: +# Client().get("/span_name/1234/") +# span_list = self.exporter.get_finished_spans() +# print(span_list) +# self.assertEqual( +# span_list[0].attributes[SpanAttributes.HTTP_STATUS_CODE], 200 +# ) +# self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) +# self.assertEqual( +# parent_span.get_span_context().span_id, +# span_list[0].parent.span_id, +# ) + + +# @patch.dict( +# "os.environ", +# { +# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", +# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", +# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", +# }, +# ) +# class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase): +# @classmethod +# def setUpClass(cls): +# conf.settings.configure(ROOT_URLCONF=modules[__name__]) +# super().setUpClass() + +# def setUp(self): +# super().setUp() +# setup_test_environment() +# tracer_provider, exporter = self.create_tracer_provider() +# 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() + +# def test_http_custom_request_headers_in_span_attributes(self): +# expected = { +# "http.request.header.custom_test_header_1": ( +# "test-header-value-1", +# ), +# "http.request.header.custom_test_header_2": ( +# "test-header-value-2", +# ), +# "http.request.header.regex_test_header_1": ("Regex Test Value 1",), +# "http.request.header.regex_test_header_2": ( +# "RegexTestValue2,RegexTestValue3", +# ), +# "http.request.header.my_secret_header": ("[REDACTED]",), +# } +# Client( +# HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", +# HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", +# HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1", +# HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3", +# HTTP_MY_SECRET_HEADER="My Secret Value", +# ).get("/traced/") +# spans = self.exporter.get_finished_spans() +# self.assertEqual(len(spans), 1) + +# span = spans[0] +# self.assertEqual(span.kind, SpanKind.SERVER) +# self.assertSpanHasAttributes(span, expected) +# self.memory_exporter.clear() + +# def test_http_custom_request_headers_not_in_span_attributes(self): +# not_expected = { +# "http.request.header.custom_test_header_2": ( +# "test-header-value-2", +# ), +# } +# Client(HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1").get("/traced/") +# spans = self.exporter.get_finished_spans() +# self.assertEqual(len(spans), 1) + +# span = spans[0] +# self.assertEqual(span.kind, SpanKind.SERVER) +# for key, _ in not_expected.items(): +# self.assertNotIn(key, span.attributes) +# self.memory_exporter.clear() + +# def test_http_custom_response_headers_in_span_attributes(self): +# expected = { +# "http.response.header.custom_test_header_1": ( +# "test-header-value-1", +# ), +# "http.response.header.custom_test_header_2": ( +# "test-header-value-2", +# ), +# "http.response.header.my_custom_regex_header_1": ( +# "my-custom-regex-value-1,my-custom-regex-value-2", +# ), +# "http.response.header.my_custom_regex_header_2": ( +# "my-custom-regex-value-3,my-custom-regex-value-4", +# ), +# "http.response.header.my_secret_header": ("[REDACTED]",), +# } +# Client().get("/traced_custom_header/") +# spans = self.exporter.get_finished_spans() +# self.assertEqual(len(spans), 1) + +# span = spans[0] +# self.assertEqual(span.kind, SpanKind.SERVER) +# self.assertSpanHasAttributes(span, expected) +# self.memory_exporter.clear() + +# def test_http_custom_response_headers_not_in_span_attributes(self): +# not_expected = { +# "http.response.header.custom_test_header_3": ( +# "test-header-value-3", +# ), +# } +# Client().get("/traced_custom_header/") +# spans = self.exporter.get_finished_spans() +# self.assertEqual(len(spans), 1) + +# span = spans[0] +# self.assertEqual(span.kind, SpanKind.SERVER) +# for key, _ in not_expected.items(): +# self.assertNotIn(key, span.attributes) +# self.memory_exporter.clear() From 240e9940a17bba3fd99d6963c2252d53e59a0e8a Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 18 Jul 2024 13:51:42 -0700 Subject: [PATCH 04/19] chagelog --- CHANGELOG.md | 4 +- .../tests/test_middleware.py | 1392 ++++++++--------- 2 files changed, 693 insertions(+), 703 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e5a041c7..2828d235ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-instrumentation-django` Add `http.target` to Django duration metric attributes ([#2624](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2624)) - `opentelemetry-instrumentation-django` Implement new semantic convention opt-in with stable http semantic conventions - ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) + ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) ### Breaking changes @@ -47,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` instrumentation ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware - ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) + ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) ### Fixed - Handle `redis.exceptions.WatchError` as a non-error event in redis instrumentation diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 44c41a8f89..71271c1bc4 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -26,8 +26,6 @@ from opentelemetry import trace from opentelemetry.instrumentation._semconv import ( OTEL_SEMCONV_STABILITY_OPT_IN, - _OpenTelemetryStabilitySignalType, - _HTTPStabilityMode, _OpenTelemetrySemanticConventionStability, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, @@ -51,7 +49,6 @@ from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.semconv.attributes.client_attributes import ( CLIENT_ADDRESS, - CLIENT_PORT, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, @@ -66,18 +63,11 @@ NETWORK_PROTOCOL_VERSION, ) from opentelemetry.semconv.attributes.server_attributes import ( - SERVER_ADDRESS, SERVER_PORT, ) from opentelemetry.semconv.attributes.url_attributes import ( - URL_FULL, - URL_PATH, - URL_QUERY, URL_SCHEME, ) -from opentelemetry.semconv.attributes.user_agent_attributes import ( - USER_AGENT_ORIGINAL, -) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import ( @@ -192,380 +182,380 @@ def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() - # def test_templated_route_get(self): - # Client().get("/route/2020/template/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual( - # span.name, - # ( - # "GET ^route/(?P[0-9]{4})/template/$" - # if DJANGO_2_2 - # else "GET" - # ), - # ) - # 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://testserver/route/2020/template/", - # ) - # if DJANGO_2_2: - # self.assertEqual( - # span.attributes[SpanAttributes.HTTP_ROUTE], - # "^route/(?P[0-9]{4})/template/$", - # ) - # self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") - # self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) - - # def test_traced_get(self): - # Client().get("/traced/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") - # 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://testserver/traced/", - # ) - # if DJANGO_2_2: - # 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) - - # def test_traced_get_new_semconv(self): - # Client().get("/traced/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") - # self.assertEqual(span.kind, SpanKind.SERVER) - # self.assertEqual(span.status.status_code, StatusCode.UNSET) - # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") - # self.assertEqual(span.attributes[URL_SCHEME], "http") - # self.assertEqual(span.attributes[SERVER_PORT], 80) - # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") - # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - # if DJANGO_2_2: - # self.assertEqual( - # span.attributes[HTTP_ROUTE], "^traced/" - # ) - # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) - - # def test_traced_get_both_semconv(self): - # Client().get("/traced/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") - # 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://testserver/traced/", - # ) - # if DJANGO_2_2: - # 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) - # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") - # self.assertEqual(span.attributes[URL_SCHEME], "http") - # self.assertEqual(span.attributes[SERVER_PORT], 80) - # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") - # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - # if DJANGO_2_2: - # self.assertEqual( - # span.attributes[HTTP_ROUTE], "^traced/" - # ) - # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) - -# 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 -# 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) - -# def test_empty_path(self): -# Client().get("/") - -# spans = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(spans), 1) - -# span = spans[0] - -# self.assertEqual(span.name, "GET empty") - - # def test_traced_post(self): - # Client().post("/traced/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") - # 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://testserver/traced/", - # ) - # if DJANGO_2_2: - # 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) - - # def test_traced_post_new_semconv(self): - # Client().post("/traced/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") - # self.assertEqual(span.kind, SpanKind.SERVER) - # self.assertEqual(span.status.status_code, StatusCode.UNSET) - # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") - # self.assertEqual(span.attributes[URL_SCHEME], "http") - # self.assertEqual(span.attributes[SERVER_PORT], 80) - # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") - # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - # if DJANGO_2_2: - # self.assertEqual( - # span.attributes[HTTP_ROUTE], "^traced/" - # ) - # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) - - # def test_traced_post_both_semconv(self): - # Client().post("/traced/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") - # 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://testserver/traced/", - # ) - # self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") - # self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) - # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") - # self.assertEqual(span.attributes[URL_SCHEME], "http") - # self.assertEqual(span.attributes[SERVER_PORT], 80) - # self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") - # self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - # if DJANGO_2_2: - # self.assertEqual( - # span.attributes[HTTP_ROUTE], "^traced/" - # ) - # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) - - # def test_error(self): - # with self.assertRaises(ValueError): - # Client().get("/error/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") - # 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://testserver/error/", - # ) - # if DJANGO_2_2: - # 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" - # ) - - # def test_error_new_semconv(self): - # with self.assertRaises(ValueError): - # Client().get("/error/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") - # self.assertEqual(span.kind, SpanKind.SERVER) - # self.assertEqual(span.status.status_code, StatusCode.ERROR) - # self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") - # if DJANGO_2_2: - # self.assertEqual( - # span.attributes[HTTP_ROUTE], "^error/" - # ) - # self.assertEqual(span.attributes[URL_SCHEME], "http") - # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) - - # self.assertEqual(len(span.events), 1) - # event = span.events[0] - # self.assertEqual(event.name, "exception") - # self.assertEqual( - # event.attributes[EXCEPTION_TYPE], "ValueError" - # ) - # self.assertEqual( - # event.attributes[EXCEPTION_MESSAGE], "error" - # ) - - # def test_error_both_semconv(self): - # with self.assertRaises(ValueError): - # Client().get("/error/") - - # spans = self.memory_exporter.get_finished_spans() - # self.assertEqual(len(spans), 1) - - # span = spans[0] - - # self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") - # 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://testserver/error/", - # ) - # if DJANGO_2_2: - # 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(span.attributes[HTTP_REQUEST_METHOD], "GET") - # if DJANGO_2_2: - # self.assertEqual( - # span.attributes[HTTP_ROUTE], "^error/" - # ) - # self.assertEqual(span.attributes[URL_SCHEME], "http") - # self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) - - # self.assertEqual(len(span.events), 1) - # event = span.events[0] - # self.assertEqual(event.name, "exception") - # self.assertEqual( - # event.attributes[EXCEPTION_TYPE], "ValueError" - # ) - # self.assertEqual( - # event.attributes[EXCEPTION_MESSAGE], "error" - # ) - -# def test_exclude_lists(self): -# client = Client() -# client.get("/excluded_arg/123") -# span_list = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(span_list), 0) - -# client.get("/excluded_arg/125") -# span_list = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(span_list), 1) - -# client.get("/excluded_noarg/") -# span_list = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(span_list), 1) - -# client.get("/excluded_noarg2/") -# span_list = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(span_list), 1) - -# def test_exclude_lists_through_instrument(self): -# _django_instrumentor.uninstrument() -# _django_instrumentor.instrument(excluded_urls="excluded_explicit") -# client = Client() -# client.get("/excluded_explicit") -# span_list = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(span_list), 0) - -# client.get("/excluded_arg/123") -# span_list = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(span_list), 1) - - # def test_span_name(self): - # # test no query_string - # 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, - # "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", - # ) - - # def test_span_name_for_query_string(self): - # """ - # request not have query string - # """ - # 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, - # "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", - # ) - - # def test_span_name_404(self): - # 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, "GET") + def test_templated_route_get(self): + Client().get("/route/2020/template/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual( + span.name, + ( + "GET ^route/(?P[0-9]{4})/template/$" + if DJANGO_2_2 + else "GET" + ), + ) + 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://testserver/route/2020/template/", + ) + if DJANGO_2_2: + self.assertEqual( + span.attributes[SpanAttributes.HTTP_ROUTE], + "^route/(?P[0-9]{4})/template/$", + ) + self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + + def test_traced_get(self): + Client().get("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") + 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://testserver/traced/", + ) + if DJANGO_2_2: + 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) + + def test_traced_get_new_semconv(self): + Client().get("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + if DJANGO_2_2: + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + def test_traced_get_both_semconv(self): + Client().get("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "GET ^traced/" if DJANGO_2_2 else "GET") + 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://testserver/traced/", + ) + if DJANGO_2_2: + 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) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + if DJANGO_2_2: + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + 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 + 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) + + def test_empty_path(self): + Client().get("/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "GET empty") + + def test_traced_post(self): + Client().post("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") + 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://testserver/traced/", + ) + if DJANGO_2_2: + 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) + + def test_traced_post_new_semconv(self): + Client().post("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + if DJANGO_2_2: + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + def test_traced_post_both_semconv(self): + Client().post("/traced/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "POST ^traced/" if DJANGO_2_2 else "POST") + 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://testserver/traced/", + ) + self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + if DJANGO_2_2: + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + def test_error(self): + with self.assertRaises(ValueError): + Client().get("/error/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") + 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://testserver/error/", + ) + if DJANGO_2_2: + 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" + ) + + def test_error_new_semconv(self): + with self.assertRaises(ValueError): + Client().get("/error/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + if DJANGO_2_2: + self.assertEqual( + span.attributes[HTTP_ROUTE], "^error/" + ) + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) + + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + self.assertEqual( + event.attributes[EXCEPTION_TYPE], "ValueError" + ) + self.assertEqual( + event.attributes[EXCEPTION_MESSAGE], "error" + ) + + def test_error_both_semconv(self): + with self.assertRaises(ValueError): + Client().get("/error/") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + + self.assertEqual(span.name, "GET ^error/" if DJANGO_2_2 else "GET") + 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://testserver/error/", + ) + if DJANGO_2_2: + 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(span.attributes[HTTP_REQUEST_METHOD], "GET") + if DJANGO_2_2: + self.assertEqual( + span.attributes[HTTP_ROUTE], "^error/" + ) + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) + + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + self.assertEqual( + event.attributes[EXCEPTION_TYPE], "ValueError" + ) + self.assertEqual( + event.attributes[EXCEPTION_MESSAGE], "error" + ) + + def test_exclude_lists(self): + client = Client() + client.get("/excluded_arg/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + client.get("/excluded_arg/125") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + client.get("/excluded_noarg/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + client.get("/excluded_noarg2/") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + def test_exclude_lists_through_instrument(self): + _django_instrumentor.uninstrument() + _django_instrumentor.instrument(excluded_urls="excluded_explicit") + client = Client() + client.get("/excluded_explicit") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) + + client.get("/excluded_arg/123") + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + + def test_span_name(self): + # test no query_string + 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, + "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", + ) + + def test_span_name_for_query_string(self): + """ + request not have query string + """ + 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, + "GET ^span_name/([0-9]{4})/$" if DJANGO_2_2 else "GET", + ) + + def test_span_name_404(self): + 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, "GET") def test_nonstandard_http_method_span_name(self): Client().request(REQUEST_METHOD="NONSTANDARD", PATH_INFO="/span_name/1234/") @@ -575,134 +565,134 @@ def test_nonstandard_http_method_span_name(self): span = span_list[0] self.assertEqual(span.name, "HTTP") -# def test_traced_request_attrs(self): -# 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) - -# 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 = 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) - -# def test_request_hook_exception(self): -# def request_hook(span, request): -# # pylint: disable=broad-exception-raised -# raise Exception("request hook exception") - -# _DjangoMiddleware._otel_request_hook = request_hook -# Client().get("/span_name/1234/") -# _DjangoMiddleware._otel_request_hook = None - -# # ensure that span ended -# finished_spans = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(finished_spans), 1) - -# def test_response_hook_exception(self): -# def response_hook(span, request, response): -# # pylint: disable=broad-exception-raised -# raise Exception("response hook exception") - -# _DjangoMiddleware._otel_response_hook = response_hook -# Client().get("/span_name/1234/") -# _DjangoMiddleware._otel_response_hook = None - -# # ensure that span ended -# finished_spans = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(finished_spans), 1) - -# def test_trace_parent(self): -# id_generator = RandomIdGenerator() -# trace_id = format_trace_id(id_generator.generate_trace_id()) -# span_id = format_span_id(id_generator.generate_span_id()) -# traceparent_value = f"00-{trace_id}-{span_id}-01" - -# Client().get( -# "/span_name/1234/", -# HTTP_TRACEPARENT=traceparent_value, -# ) -# span = self.memory_exporter.get_finished_spans()[0] - -# self.assertEqual( -# trace_id, -# format_trace_id(span.get_span_context().trace_id), -# ) -# self.assertIsNotNone(span.parent) -# self.assertEqual( -# trace_id, -# format_trace_id(span.parent.trace_id), -# ) -# self.assertEqual( -# span_id, -# format_span_id(span.parent.span_id), -# ) -# self.memory_exporter.clear() - -# def test_trace_response_headers(self): -# response = Client().get("/span_name/1234/") - -# self.assertFalse(response.has_header("Server-Timing")) -# self.memory_exporter.clear() - -# set_global_response_propagator(TraceResponsePropagator()) - -# response = Client().get("/span_name/1234/") -# self.assertTraceResponseHeaderMatchesSpan( -# response, -# self.memory_exporter.get_finished_spans()[0], -# ) -# self.memory_exporter.clear() - -# def test_uninstrument(self): -# Client().get("/route/2020/template/") -# spans = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(spans), 1) - -# self.memory_exporter.clear() -# _django_instrumentor.uninstrument() - -# Client().get("/route/2020/template/") -# spans = self.memory_exporter.get_finished_spans() -# self.assertEqual(len(spans), 0) + def test_traced_request_attrs(self): + 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) + + 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 = 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) + + def test_request_hook_exception(self): + def request_hook(span, request): + # pylint: disable=broad-exception-raised + raise Exception("request hook exception") + + _DjangoMiddleware._otel_request_hook = request_hook + Client().get("/span_name/1234/") + _DjangoMiddleware._otel_request_hook = None + + # ensure that span ended + finished_spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(finished_spans), 1) + + def test_response_hook_exception(self): + def response_hook(span, request, response): + # pylint: disable=broad-exception-raised + raise Exception("response hook exception") + + _DjangoMiddleware._otel_response_hook = response_hook + Client().get("/span_name/1234/") + _DjangoMiddleware._otel_response_hook = None + + # ensure that span ended + finished_spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(finished_spans), 1) + + def test_trace_parent(self): + id_generator = RandomIdGenerator() + trace_id = format_trace_id(id_generator.generate_trace_id()) + span_id = format_span_id(id_generator.generate_span_id()) + traceparent_value = f"00-{trace_id}-{span_id}-01" + + Client().get( + "/span_name/1234/", + HTTP_TRACEPARENT=traceparent_value, + ) + span = self.memory_exporter.get_finished_spans()[0] + + self.assertEqual( + trace_id, + format_trace_id(span.get_span_context().trace_id), + ) + self.assertIsNotNone(span.parent) + self.assertEqual( + trace_id, + format_trace_id(span.parent.trace_id), + ) + self.assertEqual( + span_id, + format_span_id(span.parent.span_id), + ) + self.memory_exporter.clear() + + def test_trace_response_headers(self): + response = Client().get("/span_name/1234/") + + self.assertFalse(response.has_header("Server-Timing")) + self.memory_exporter.clear() + + set_global_response_propagator(TraceResponsePropagator()) + + response = Client().get("/span_name/1234/") + self.assertTraceResponseHeaderMatchesSpan( + response, + self.memory_exporter.get_finished_spans()[0], + ) + self.memory_exporter.clear() + + def test_uninstrument(self): + Client().get("/route/2020/template/") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + self.memory_exporter.clear() + _django_instrumentor.uninstrument() + + Client().get("/route/2020/template/") + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) # pylint: disable=too-many-locals def test_wsgi_metrics(self): @@ -846,192 +836,192 @@ def test_wsgi_metrics_both_semconv(self): ) self.assertTrue(histrogram_data_point_seen and number_data_point_seen) -# def test_wsgi_metrics_unistrument(self): -# Client().get("/span_name/1234/") -# _django_instrumentor.uninstrument() -# Client().get("/span_name/1234/") -# metrics_list = self.memory_metrics_reader.get_metrics_data() -# for resource_metric in metrics_list.resource_metrics: -# for scope_metric in resource_metric.scope_metrics: -# for metric in scope_metric.metrics: -# for point in list(metric.data.data_points): -# if isinstance(point, HistogramDataPoint): -# self.assertEqual(1, point.count) -# if isinstance(point, NumberDataPoint): -# self.assertEqual(0, point.value) - - -# class TestMiddlewareWithTracerProvider(WsgiTestBase): -# @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 -# self.tracer_provider = tracer_provider -# _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() - -# def test_tracer_provider_traced(self): -# 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" -# ) - -# def test_django_with_wsgi_instrumented(self): -# tracer = self.tracer_provider.get_tracer(__name__) -# with tracer.start_as_current_span( -# "test", kind=SpanKind.SERVER -# ) as parent_span: -# Client().get("/span_name/1234/") -# span_list = self.exporter.get_finished_spans() -# print(span_list) -# self.assertEqual( -# span_list[0].attributes[SpanAttributes.HTTP_STATUS_CODE], 200 -# ) -# self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) -# self.assertEqual( -# parent_span.get_span_context().span_id, -# span_list[0].parent.span_id, -# ) - - -# @patch.dict( -# "os.environ", -# { -# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", -# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", -# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", -# }, -# ) -# class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase): -# @classmethod -# def setUpClass(cls): -# conf.settings.configure(ROOT_URLCONF=modules[__name__]) -# super().setUpClass() - -# def setUp(self): -# super().setUp() -# setup_test_environment() -# tracer_provider, exporter = self.create_tracer_provider() -# 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() - -# def test_http_custom_request_headers_in_span_attributes(self): -# expected = { -# "http.request.header.custom_test_header_1": ( -# "test-header-value-1", -# ), -# "http.request.header.custom_test_header_2": ( -# "test-header-value-2", -# ), -# "http.request.header.regex_test_header_1": ("Regex Test Value 1",), -# "http.request.header.regex_test_header_2": ( -# "RegexTestValue2,RegexTestValue3", -# ), -# "http.request.header.my_secret_header": ("[REDACTED]",), -# } -# Client( -# HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", -# HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", -# HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1", -# HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3", -# HTTP_MY_SECRET_HEADER="My Secret Value", -# ).get("/traced/") -# spans = self.exporter.get_finished_spans() -# self.assertEqual(len(spans), 1) - -# span = spans[0] -# self.assertEqual(span.kind, SpanKind.SERVER) -# self.assertSpanHasAttributes(span, expected) -# self.memory_exporter.clear() - -# def test_http_custom_request_headers_not_in_span_attributes(self): -# not_expected = { -# "http.request.header.custom_test_header_2": ( -# "test-header-value-2", -# ), -# } -# Client(HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1").get("/traced/") -# spans = self.exporter.get_finished_spans() -# self.assertEqual(len(spans), 1) - -# span = spans[0] -# self.assertEqual(span.kind, SpanKind.SERVER) -# for key, _ in not_expected.items(): -# self.assertNotIn(key, span.attributes) -# self.memory_exporter.clear() - -# def test_http_custom_response_headers_in_span_attributes(self): -# expected = { -# "http.response.header.custom_test_header_1": ( -# "test-header-value-1", -# ), -# "http.response.header.custom_test_header_2": ( -# "test-header-value-2", -# ), -# "http.response.header.my_custom_regex_header_1": ( -# "my-custom-regex-value-1,my-custom-regex-value-2", -# ), -# "http.response.header.my_custom_regex_header_2": ( -# "my-custom-regex-value-3,my-custom-regex-value-4", -# ), -# "http.response.header.my_secret_header": ("[REDACTED]",), -# } -# Client().get("/traced_custom_header/") -# spans = self.exporter.get_finished_spans() -# self.assertEqual(len(spans), 1) - -# span = spans[0] -# self.assertEqual(span.kind, SpanKind.SERVER) -# self.assertSpanHasAttributes(span, expected) -# self.memory_exporter.clear() - -# def test_http_custom_response_headers_not_in_span_attributes(self): -# not_expected = { -# "http.response.header.custom_test_header_3": ( -# "test-header-value-3", -# ), -# } -# Client().get("/traced_custom_header/") -# spans = self.exporter.get_finished_spans() -# self.assertEqual(len(spans), 1) - -# span = spans[0] -# self.assertEqual(span.kind, SpanKind.SERVER) -# for key, _ in not_expected.items(): -# self.assertNotIn(key, span.attributes) -# self.memory_exporter.clear() + def test_wsgi_metrics_unistrument(self): + Client().get("/span_name/1234/") + _django_instrumentor.uninstrument() + Client().get("/span_name/1234/") + metrics_list = self.memory_metrics_reader.get_metrics_data() + for resource_metric in metrics_list.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + for point in list(metric.data.data_points): + if isinstance(point, HistogramDataPoint): + self.assertEqual(1, point.count) + if isinstance(point, NumberDataPoint): + self.assertEqual(0, point.value) + + +class TestMiddlewareWithTracerProvider(WsgiTestBase): + @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 + self.tracer_provider = tracer_provider + _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() + + def test_tracer_provider_traced(self): + 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" + ) + + def test_django_with_wsgi_instrumented(self): + tracer = self.tracer_provider.get_tracer(__name__) + with tracer.start_as_current_span( + "test", kind=SpanKind.SERVER + ) as parent_span: + Client().get("/span_name/1234/") + span_list = self.exporter.get_finished_spans() + print(span_list) + self.assertEqual( + span_list[0].attributes[SpanAttributes.HTTP_STATUS_CODE], 200 + ) + self.assertEqual(trace.SpanKind.INTERNAL, span_list[0].kind) + self.assertEqual( + parent_span.get_span_context().span_id, + span_list[0].parent.span_id, + ) + + +@patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*", + }, +) +class TestMiddlewareWsgiWithCustomHeaders(WsgiTestBase): + @classmethod + def setUpClass(cls): + conf.settings.configure(ROOT_URLCONF=modules[__name__]) + super().setUpClass() + + def setUp(self): + super().setUp() + setup_test_environment() + tracer_provider, exporter = self.create_tracer_provider() + 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() + + def test_http_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.request.header.regex_test_header_1": ("Regex Test Value 1",), + "http.request.header.regex_test_header_2": ( + "RegexTestValue2,RegexTestValue3", + ), + "http.request.header.my_secret_header": ("[REDACTED]",), + } + Client( + HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", + HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", + HTTP_REGEX_TEST_HEADER_1="Regex Test Value 1", + HTTP_REGEX_TEST_HEADER_2="RegexTestValue2,RegexTestValue3", + HTTP_MY_SECRET_HEADER="My Secret Value", + ).get("/traced/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + self.memory_exporter.clear() + + def test_http_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + Client(HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1").get("/traced/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + self.memory_exporter.clear() + + def test_http_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + "http.response.header.my_custom_regex_header_1": ( + "my-custom-regex-value-1,my-custom-regex-value-2", + ), + "http.response.header.my_custom_regex_header_2": ( + "my-custom-regex-value-3,my-custom-regex-value-4", + ), + "http.response.header.my_secret_header": ("[REDACTED]",), + } + Client().get("/traced_custom_header/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + self.memory_exporter.clear() + + def test_http_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + Client().get("/traced_custom_header/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + self.memory_exporter.clear() From a454db074b74b24b8f2023188183538cdbe738e8 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 18 Jul 2024 14:33:07 -0700 Subject: [PATCH 05/19] asgi --- .../tests/test_middleware_asgi.py | 262 +++++++++++++++++- 1 file changed, 261 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index 0e2472d15e..c036c0cde2 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -24,6 +24,10 @@ from django.test.utils import setup_test_environment, teardown_test_environment from opentelemetry import trace as trace_api +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.django import ( DjangoInstrumentor, _DjangoMiddleware, @@ -35,6 +39,27 @@ from opentelemetry.sdk import resources from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.id_generator import RandomIdGenerator +from opentelemetry.semconv.attributes.client_attributes import ( + CLIENT_ADDRESS, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, + HTTP_ROUTE, +) +from opentelemetry.semconv.attributes.exception_attributes import ( + EXCEPTION_MESSAGE, + EXCEPTION_TYPE, +) +from opentelemetry.semconv.attributes.network_attributes import ( + NETWORK_PROTOCOL_VERSION, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_PORT, +) +from opentelemetry.semconv.attributes.url_attributes import ( + URL_SCHEME, +) from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import ( @@ -96,15 +121,25 @@ def setUpClass(cls): def setUp(self): super().setUp() setup_test_environment() - _django_instrumentor.instrument() + test_name = "" + if hasattr(self, "_testMethodName"): + test_name = self._testMethodName + sem_conv_mode = "default" + if "new_semconv" in test_name: + sem_conv_mode = "http" + elif "both_semconv" in test_name: + sem_conv_mode = "http/dup" 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", + OTEL_SEMCONV_STABILITY_OPT_IN: sem_conv_mode, }, ) + _OpenTelemetrySemanticConventionStability._initialized = False self.env_patch.start() + _django_instrumentor.instrument() self.exclude_patch = patch( "opentelemetry.instrumentation.django.middleware.otel_middleware._DjangoMiddleware._excluded_urls", get_excluded_urls("DJANGO"), @@ -152,6 +187,57 @@ async def test_templated_route_get(self): self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + async def test_templated_route_get_new_semconv(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, "GET ^route/(?P[0-9]{4})/template/$") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + self.assertEqual( + span.attributes[HTTP_ROUTE], + "^route/(?P[0-9]{4})/template/$", + ) + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + async def test_templated_route_get_both_semconv(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, "GET ^route/(?P[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_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + self.assertEqual( + span.attributes[HTTP_ROUTE], + "^route/(?P[0-9]{4})/template/$", + ) + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + async def test_traced_get(self): await self.async_client.get("/traced/") @@ -174,6 +260,55 @@ async def test_traced_get(self): self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + async def test_traced_get_new_semconv(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, "GET ^traced/") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + async def test_traced_get_both_semconv(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, "GET ^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_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + async def test_not_recording(self): mock_tracer = Mock() mock_span = Mock() @@ -209,6 +344,55 @@ async def test_traced_post(self): self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + async def test_traced_post_new_semconv(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, "POST ^traced/") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.UNSET) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + + async def test_traced_post_both_semconv(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, "POST ^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_SCHEME], "http") + self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "POST") + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[SERVER_PORT], 80) + self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") + self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") + self.assertEqual( + span.attributes[HTTP_ROUTE], "^traced/" + ) + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + async def test_error(self): with self.assertRaises(ValueError): await self.async_client.get("/error/") @@ -239,6 +423,74 @@ async def test_error(self): self.assertEqual( event.attributes[SpanAttributes.EXCEPTION_MESSAGE], "error" ) + + async def test_error_new_semconv(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, "GET ^error/") + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertEqual(span.status.status_code, StatusCode.ERROR) + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual( + span.attributes[HTTP_ROUTE], "^error/" + ) + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) + + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + self.assertEqual( + event.attributes[EXCEPTION_TYPE], "ValueError" + ) + self.assertEqual( + event.attributes[EXCEPTION_MESSAGE], "error" + ) + + async def test_error_both_semconv(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, "GET ^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(span.attributes[HTTP_REQUEST_METHOD], "GET") + self.assertEqual( + span.attributes[HTTP_ROUTE], "^error/" + ) + self.assertEqual(span.attributes[URL_SCHEME], "http") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) + + self.assertEqual(len(span.events), 1) + event = span.events[0] + self.assertEqual(event.name, "exception") + self.assertEqual( + event.attributes[EXCEPTION_TYPE], "ValueError" + ) + self.assertEqual( + event.attributes[EXCEPTION_MESSAGE], "error" + ) async def test_exclude_lists(self): await self.async_client.get("/excluded_arg/123") @@ -285,6 +537,14 @@ async def test_span_name_404(self): span = span_list[0] self.assertEqual(span.name, "GET") + async def test_nonstandard_http_method_span_name(self): + await self.async_client.request(method="NONSTANDARD", path="/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, "HTTP") + 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() From 80e1ce349769524cdfcb7a18a92318f51a5ce2ab Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 18 Jul 2024 16:01:20 -0700 Subject: [PATCH 06/19] lint --- .../instrumentation/django/__init__.py | 4 +- .../django/middleware/otel_middleware.py | 13 ++--- .../tests/test_middleware.py | 44 +++++---------- .../tests/test_middleware_asgi.py | 54 ++++++------------- .../tests/views.py | 12 ++--- 5 files changed, 45 insertions(+), 82 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index e298d2dbb1..651df12043 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -353,8 +353,8 @@ def _instrument(self, **kwargs): description="Duration of HTTP server requests.", unit="s", ) - _DjangoMiddleware._active_request_counter = create_http_server_active_requests( - meter + _DjangoMiddleware._active_request_counter = ( + create_http_server_active_requests(meter) ) # This can not be solved, but is an inherent problem of this approach: # the order of middleware entries matters, and here you have no control diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index 89ce07b972..88e02ac1cf 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -172,9 +172,9 @@ class _DjangoMiddleware(MiddlewareMixin): _sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT _otel_request_hook: Callable[[Span, HttpRequest], None] = None - _otel_response_hook: Callable[[Span, HttpRequest, HttpResponse], None] = ( - None - ) + _otel_response_hook: Callable[ + [Span, HttpRequest, HttpResponse], None + ] = None @staticmethod def _get_span_name(request): @@ -245,9 +245,9 @@ def process_request(self, request): self._sem_conv_opt_in_mode, ) - request.META[self._environ_active_request_attr_key] = ( - active_requests_count_attrs - ) + request.META[ + self._environ_active_request_attr_key + ] = active_requests_count_attrs # Pass all of attributes to duration key because we will filter during response request.META[self._environ_duration_attr_key] = attributes self._active_request_counter.add(1, active_requests_count_attrs) @@ -459,6 +459,7 @@ def process_response(self, request, response): return response + def _parse_duration_attrs( req_attrs, sem_conv_opt_in_mode=_HTTPStabilityMode.DEFAULT ): diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 71271c1bc4..e18c7ca547 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -253,9 +253,7 @@ def test_traced_get_new_semconv(self): self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") if DJANGO_2_2: - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) def test_traced_get_both_semconv(self): @@ -286,9 +284,7 @@ def test_traced_get_both_semconv(self): self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") if DJANGO_2_2: - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) def test_not_recording(self): @@ -354,9 +350,7 @@ def test_traced_post_new_semconv(self): self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") if DJANGO_2_2: - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) def test_traced_post_both_semconv(self): @@ -383,9 +377,7 @@ def test_traced_post_both_semconv(self): self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") if DJANGO_2_2: - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) def test_error(self): @@ -436,21 +428,15 @@ def test_error_new_semconv(self): self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") if DJANGO_2_2: - self.assertEqual( - span.attributes[HTTP_ROUTE], "^error/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") - self.assertEqual( - event.attributes[EXCEPTION_TYPE], "ValueError" - ) - self.assertEqual( - event.attributes[EXCEPTION_MESSAGE], "error" - ) + self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") + self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") def test_error_both_semconv(self): with self.assertRaises(ValueError): @@ -477,21 +463,15 @@ def test_error_both_semconv(self): self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 500) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") if DJANGO_2_2: - self.assertEqual( - span.attributes[HTTP_ROUTE], "^error/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") - self.assertEqual( - event.attributes[EXCEPTION_TYPE], "ValueError" - ) - self.assertEqual( - event.attributes[EXCEPTION_MESSAGE], "error" - ) + self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") + self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") def test_exclude_lists(self): client = Client() @@ -558,7 +538,9 @@ def test_span_name_404(self): self.assertEqual(span.name, "GET") def test_nonstandard_http_method_span_name(self): - Client().request(REQUEST_METHOD="NONSTANDARD", PATH_INFO="/span_name/1234/") + Client().request( + REQUEST_METHOD="NONSTANDARD", PATH_INFO="/span_name/1234/" + ) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index c036c0cde2..d38f6293aa 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -276,9 +276,7 @@ async def test_traced_get_new_semconv(self): self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_traced_get_both_semconv(self): @@ -304,9 +302,7 @@ async def test_traced_get_both_semconv(self): self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_not_recording(self): @@ -360,10 +356,8 @@ async def test_traced_post_new_semconv(self): self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) - self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_traced_post_both_semconv(self): await self.async_client.post("/traced/") @@ -388,10 +382,8 @@ async def test_traced_post_both_semconv(self): self.assertEqual(span.attributes[SERVER_PORT], 80) self.assertEqual(span.attributes[CLIENT_ADDRESS], "127.0.0.1") self.assertEqual(span.attributes[NETWORK_PROTOCOL_VERSION], "1.1") - self.assertEqual( - span.attributes[HTTP_ROUTE], "^traced/" - ) - self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) + self.assertEqual(span.attributes[HTTP_ROUTE], "^traced/") + self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 200) async def test_error(self): with self.assertRaises(ValueError): @@ -423,7 +415,7 @@ async def test_error(self): self.assertEqual( event.attributes[SpanAttributes.EXCEPTION_MESSAGE], "error" ) - + async def test_error_new_semconv(self): with self.assertRaises(ValueError): await self.async_client.get("/error/") @@ -437,21 +429,15 @@ async def test_error_new_semconv(self): self.assertEqual(span.kind, SpanKind.SERVER) self.assertEqual(span.status.status_code, StatusCode.ERROR) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") - self.assertEqual( - span.attributes[HTTP_ROUTE], "^error/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") - self.assertEqual( - event.attributes[EXCEPTION_TYPE], "ValueError" - ) - self.assertEqual( - event.attributes[EXCEPTION_MESSAGE], "error" - ) + self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") + self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") async def test_error_both_semconv(self): with self.assertRaises(ValueError): @@ -470,27 +456,19 @@ async def test_error_both_semconv(self): 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_ROUTE], "^error/") self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http") self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 500) self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "GET") - self.assertEqual( - span.attributes[HTTP_ROUTE], "^error/" - ) + self.assertEqual(span.attributes[HTTP_ROUTE], "^error/") self.assertEqual(span.attributes[URL_SCHEME], "http") self.assertEqual(span.attributes[HTTP_RESPONSE_STATUS_CODE], 500) self.assertEqual(len(span.events), 1) event = span.events[0] self.assertEqual(event.name, "exception") - self.assertEqual( - event.attributes[EXCEPTION_TYPE], "ValueError" - ) - self.assertEqual( - event.attributes[EXCEPTION_MESSAGE], "error" - ) + self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") + self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") async def test_exclude_lists(self): await self.async_client.get("/excluded_arg/123") @@ -538,7 +516,9 @@ async def test_span_name_404(self): self.assertEqual(span.name, "GET") async def test_nonstandard_http_method_span_name(self): - await self.async_client.request(method="NONSTANDARD", path="/span_name/1234/") + await self.async_client.request( + method="NONSTANDARD", path="/span_name/1234/" + ) span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index 6310664100..452a7c0fdd 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -35,12 +35,12 @@ def response_with_custom_header(request): response = HttpResponse() response["custom-test-header-1"] = "test-header-value-1" response["custom-test-header-2"] = "test-header-value-2" - response["my-custom-regex-header-1"] = ( - "my-custom-regex-value-1,my-custom-regex-value-2" - ) - response["my-custom-regex-header-2"] = ( - "my-custom-regex-value-3,my-custom-regex-value-4" - ) + response[ + "my-custom-regex-header-1" + ] = "my-custom-regex-value-1,my-custom-regex-value-2" + response[ + "my-custom-regex-header-2" + ] = "my-custom-regex-value-3,my-custom-regex-value-4" response["my-secret-header"] = "my-secret-value" return response From 83964d99c7a145c7c370a8253fdaa3fac9c8390c Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Thu, 18 Jul 2024 16:51:28 -0700 Subject: [PATCH 07/19] lint --- .../django/middleware/otel_middleware.py | 12 ++++++------ .../tests/views.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index 88e02ac1cf..59ff534020 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -172,9 +172,9 @@ class _DjangoMiddleware(MiddlewareMixin): _sem_conv_opt_in_mode = _HTTPStabilityMode.DEFAULT _otel_request_hook: Callable[[Span, HttpRequest], None] = None - _otel_response_hook: Callable[ - [Span, HttpRequest, HttpResponse], None - ] = None + _otel_response_hook: Callable[[Span, HttpRequest, HttpResponse], None] = ( + None + ) @staticmethod def _get_span_name(request): @@ -245,9 +245,9 @@ def process_request(self, request): self._sem_conv_opt_in_mode, ) - request.META[ - self._environ_active_request_attr_key - ] = active_requests_count_attrs + request.META[self._environ_active_request_attr_key] = ( + active_requests_count_attrs + ) # Pass all of attributes to duration key because we will filter during response request.META[self._environ_duration_attr_key] = attributes self._active_request_counter.add(1, active_requests_count_attrs) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index 452a7c0fdd..6310664100 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -35,12 +35,12 @@ def response_with_custom_header(request): response = HttpResponse() response["custom-test-header-1"] = "test-header-value-1" response["custom-test-header-2"] = "test-header-value-2" - response[ - "my-custom-regex-header-1" - ] = "my-custom-regex-value-1,my-custom-regex-value-2" - response[ - "my-custom-regex-header-2" - ] = "my-custom-regex-value-3,my-custom-regex-value-4" + response["my-custom-regex-header-1"] = ( + "my-custom-regex-value-1,my-custom-regex-value-2" + ) + response["my-custom-regex-header-2"] = ( + "my-custom-regex-value-3,my-custom-regex-value-4" + ) response["my-secret-header"] = "my-secret-value" return response From 1ba98d59b14e6bf28169b00fc7d55854f912cf69 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 19 Jul 2024 11:35:59 -0700 Subject: [PATCH 08/19] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d3a2a4434..a2a1f821c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2673](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2673)) - `opentelemetry-instrumentation-django` Add `http.target` to Django duration metric attributes ([#2624](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2624)) - - `opentelemetry-instrumentation-django` Implement new semantic convention opt-in with stable http semantic conventions +- `opentelemetry-instrumentation-django` Implement new semantic convention opt-in with stable http semantic conventions ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) ### Breaking changes From 6b9acafb2fadfc3adc6699e5adc0b543c21e65bb Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 19 Jul 2024 11:36:06 -0700 Subject: [PATCH 09/19] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a1f821c0..4919b01f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2580](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2580)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `asgi` middleware ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) - - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` instrumentation +- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` instrumentation ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) From c71950252a48bebe2463e1788369bb2cf7a4ed18 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 19 Jul 2024 11:36:12 -0700 Subject: [PATCH 10/19] Update CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4919b01f43..ed37c1d6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2610)) - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `fastapi` instrumentation ([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682)) - - Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware +- Populate `{method}` as `HTTP` on `_OTHER` methods from scope for `django` middleware ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) ### Fixed From ae17c7c941522e5ab1cacd574bb8aea21d91d9ba Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 19 Jul 2024 11:51:25 -0700 Subject: [PATCH 11/19] lint --- .../tests/test_middleware.py | 4 ++-- .../tests/test_middleware_asgi.py | 18 ++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index e18c7ca547..01b35ce724 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -734,7 +734,7 @@ def test_wsgi_metrics_new_semconv(self): for _ in range(3): response = Client().get("/span_name/1234/") self.assertEqual(response.status_code, 200) - duration = default_timer() - start + duration_s = default_timer() - start metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histrogram_data_point_seen = False @@ -753,7 +753,7 @@ def test_wsgi_metrics_new_semconv(self): self.assertEqual(point.count, 3) histrogram_data_point_seen = True self.assertAlmostEqual( - duration, point.sum, places=2 + duration_s, point.sum, places=2 ) if isinstance(point, NumberDataPoint): number_data_point_seen = True diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index d38f6293aa..55a6f36132 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -39,27 +39,21 @@ from opentelemetry.sdk import resources from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.id_generator import RandomIdGenerator -from opentelemetry.semconv.attributes.client_attributes import ( - CLIENT_ADDRESS, +from opentelemetry.semconv.attributes.client_attributes import CLIENT_ADDRESS +from opentelemetry.semconv.attributes.exception_attributes import ( + EXCEPTION_MESSAGE, + EXCEPTION_TYPE, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) -from opentelemetry.semconv.attributes.exception_attributes import ( - EXCEPTION_MESSAGE, - EXCEPTION_TYPE, -) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) -from opentelemetry.semconv.attributes.server_attributes import ( - SERVER_PORT, -) -from opentelemetry.semconv.attributes.url_attributes import ( - URL_SCHEME, -) +from opentelemetry.semconv.attributes.server_attributes import SERVER_PORT +from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.test_base import TestBase from opentelemetry.trace import ( From 31fe38ff319bf230cb2110d9e9d75aa2d766add3 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Fri, 19 Jul 2024 12:02:54 -0700 Subject: [PATCH 12/19] Update test_middleware.py --- .../tests/test_middleware.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 01b35ce724..4506ddd7f7 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -47,27 +47,21 @@ ) from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.id_generator import RandomIdGenerator -from opentelemetry.semconv.attributes.client_attributes import ( - CLIENT_ADDRESS, +from opentelemetry.semconv.attributes.client_attributes import CLIENT_ADDRESS +from opentelemetry.semconv.attributes.exception_attributes import ( + EXCEPTION_MESSAGE, + EXCEPTION_TYPE, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) -from opentelemetry.semconv.attributes.exception_attributes import ( - EXCEPTION_MESSAGE, - EXCEPTION_TYPE, -) from opentelemetry.semconv.attributes.network_attributes import ( NETWORK_PROTOCOL_VERSION, ) -from opentelemetry.semconv.attributes.server_attributes import ( - SERVER_PORT, -) -from opentelemetry.semconv.attributes.url_attributes import ( - URL_SCHEME, -) +from opentelemetry.semconv.attributes.server_attributes import SERVER_PORT +from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.test.wsgitestutil import WsgiTestBase from opentelemetry.trace import ( From 1a38232c9a2d6036cf2a99f863e4d5934a642f0b Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 09:07:22 -0700 Subject: [PATCH 13/19] lint --- .../django/middleware/otel_middleware.py | 18 +----------------- .../tests/test_middleware.py | 2 +- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index 59ff534020..65900e4349 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -25,27 +25,13 @@ from opentelemetry.instrumentation._semconv import ( _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, - _get_schema_url, _HTTPStabilityMode, - _OpenTelemetrySemanticConventionStability, - _OpenTelemetryStabilitySignalType, _report_new, _report_old, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, - _set_http_flavor_version, - _set_http_host, - _set_http_method, - _set_http_net_host_port, - _set_http_peer_ip, - _set_http_peer_port_server, - _set_http_scheme, - _set_http_target, - _set_http_url, - _set_http_user_agent, - _set_status, ) from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, @@ -65,9 +51,7 @@ collect_request_attributes as wsgi_collect_request_attributes, ) from opentelemetry.instrumentation.wsgi import wsgi_getter -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_ROUTE, -) +from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import ( diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 4506ddd7f7..f9c6721c44 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -797,7 +797,7 @@ def test_wsgi_metrics_both_semconv(self): histrogram_data_point_seen = True if metric.name == "http.server.request.duration": self.assertAlmostEqual( - duration_s, point.sum, places=2 + duration_s, point.sum, places=1 ) elif metric.name == "http.server.duration": self.assertAlmostEqual( From 9adb5889461f15fc7b7d8e0d548915a1e7ccc452 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 09:21:02 -0700 Subject: [PATCH 14/19] lint --- .../tests/test_middleware.py | 4 +++- .../tests/test_middleware_asgi.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index f9c6721c44..01750c8d15 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -13,6 +13,7 @@ # limitations under the License. # pylint: disable=E0611 +# pylint: disable=too-many-lines from sys import modules from timeit import default_timer @@ -759,6 +760,7 @@ def test_wsgi_metrics_new_semconv(self): self.assertTrue(histrogram_data_point_seen and number_data_point_seen) # pylint: disable=too-many-locals + # pylint: disable=too-many-nested-blocks def test_wsgi_metrics_both_semconv(self): _expected_metric_names = [ "http.server.duration", @@ -801,7 +803,7 @@ def test_wsgi_metrics_both_semconv(self): ) elif metric.name == "http.server.duration": self.assertAlmostEqual( - duration, point.sum, delta=5 + duration, point.sum, delta=10 ) if isinstance(point, NumberDataPoint): number_data_point_seen = True diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index 55a6f36132..53890b744e 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -106,6 +106,7 @@ @pytest.mark.skipif( not DJANGO_3_1, reason="AsyncClient implemented since Django 3.1" ) +# pylint: disable=too-many-public-methods class TestMiddlewareAsgi(SimpleTestCase, TestBase): @classmethod def setUpClass(cls): From 3af7e5d9266678f74f9b00a9bda4d84e6ec2f1df Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 10:44:28 -0700 Subject: [PATCH 15/19] fixes --- .../django/middleware/otel_middleware.py | 12 +++---- .../tests/test_middleware.py | 32 ++++++++++++++++++- .../tests/test_wsgi_middleware.py | 7 +++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index 65900e4349..4530a16506 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -421,18 +421,18 @@ def process_response(self, request, response): activation.__exit__(None, None, None) if request_start_time is not None: - duration_attrs_old = _parse_duration_attrs( - duration_attrs, _HTTPStabilityMode.DEFAULT - ) - duration_attrs_new = _parse_duration_attrs( - duration_attrs, _HTTPStabilityMode.HTTP - ) duration_s = default_timer() - request_start_time if self._duration_histogram_old: + duration_attrs_old = _parse_duration_attrs( + duration_attrs, _HTTPStabilityMode.DEFAULT + ) self._duration_histogram_old.record( max(round(duration_s * 1000), 0), duration_attrs_old ) if self._duration_histogram_new: + duration_attrs_new = _parse_duration_attrs( + duration_attrs, _HTTPStabilityMode.HTTP + ) self._duration_histogram_new.record( max(duration_s, 0), duration_attrs_new ) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 01750c8d15..40a45bb118 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -49,12 +49,14 @@ from opentelemetry.sdk.trace import Span from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.semconv.attributes.client_attributes import CLIENT_ADDRESS +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_MESSAGE, EXCEPTION_TYPE, ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, + HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) @@ -432,6 +434,7 @@ def test_error_new_semconv(self): self.assertEqual(event.name, "exception") self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") + self.assertEqual(span.attributes[ERROR_TYPE], '500') def test_error_both_semconv(self): with self.assertRaises(ValueError): @@ -467,6 +470,7 @@ def test_error_both_semconv(self): self.assertEqual(event.name, "exception") self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") + self.assertEqual(span.attributes[ERROR_TYPE], '500') def test_exclude_lists(self): client = Client() @@ -541,6 +545,32 @@ def test_nonstandard_http_method_span_name(self): span = span_list[0] self.assertEqual(span.name, "HTTP") + self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "_OTHER") + + def test_nonstandard_http_method_span_name_new_semconv(self): + Client().request( + REQUEST_METHOD="NONSTANDARD", PATH_INFO="/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, "HTTP") + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD") + + def test_nonstandard_http_method_span_name_both_semconv(self): + Client().request( + REQUEST_METHOD="NONSTANDARD", PATH_INFO="/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, "HTTP") + self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "_OTHER") + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD") def test_traced_request_attrs(self): Client().get("/span_name/1234/", CONTENT_TYPE="test/ct") @@ -803,7 +833,7 @@ def test_wsgi_metrics_both_semconv(self): ) elif metric.name == "http.server.duration": self.assertAlmostEqual( - duration, point.sum, delta=10 + duration, point.sum, delta=100 ) if isinstance(point, NumberDataPoint): number_data_point_seen = True diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index 777d19f41d..d1f1a616c1 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -36,6 +36,9 @@ NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.semconv.attributes.error_attributes import ( + ERROR_TYPE +) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, @@ -231,10 +234,13 @@ def validate_response( self.assertEqual(self.status, "200 OK") self.assertEqual(self.response_headers, expected_headers) + expected_attributes = {} if error: self.assertIs(self.exc_info[0], error) self.assertIsInstance(self.exc_info[1], error) self.assertIsNotNone(self.exc_info[2]) + if new_sem_conv: + expected_attributes[ERROR_TYPE] = error else: self.assertIsNone(self.exc_info) @@ -242,7 +248,6 @@ def validate_response( self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, span_name) self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) - expected_attributes = {} expected_attributes_old = { SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", SpanAttributes.HTTP_SCHEME: "http", From cf1ec9ec7c89a9f56281ac1c19311d39d0a66858 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 10:53:43 -0700 Subject: [PATCH 16/19] test --- .../tests/test_middleware.py | 12 ++++++++---- .../tests/test_wsgi_middleware.py | 3 --- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 40a45bb118..364b4bcada 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -434,7 +434,7 @@ def test_error_new_semconv(self): self.assertEqual(event.name, "exception") self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") - self.assertEqual(span.attributes[ERROR_TYPE], '500') + self.assertEqual(span.attributes[ERROR_TYPE], "500") def test_error_both_semconv(self): with self.assertRaises(ValueError): @@ -470,7 +470,7 @@ def test_error_both_semconv(self): self.assertEqual(event.name, "exception") self.assertEqual(event.attributes[EXCEPTION_TYPE], "ValueError") self.assertEqual(event.attributes[EXCEPTION_MESSAGE], "error") - self.assertEqual(span.attributes[ERROR_TYPE], '500') + self.assertEqual(span.attributes[ERROR_TYPE], "500") def test_exclude_lists(self): client = Client() @@ -557,7 +557,9 @@ def test_nonstandard_http_method_span_name_new_semconv(self): span = span_list[0] self.assertEqual(span.name, "HTTP") self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") - self.assertEqual(span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD") + self.assertEqual( + span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD" + ) def test_nonstandard_http_method_span_name_both_semconv(self): Client().request( @@ -570,7 +572,9 @@ def test_nonstandard_http_method_span_name_both_semconv(self): self.assertEqual(span.name, "HTTP") self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "_OTHER") self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") - self.assertEqual(span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD") + self.assertEqual( + span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD" + ) def test_traced_request_attrs(self): Client().get("/span_name/1234/", CONTENT_TYPE="test/ct") diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index d1f1a616c1..b6b4a13f5d 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -36,9 +36,6 @@ NumberDataPoint, ) from opentelemetry.sdk.resources import Resource -from opentelemetry.semconv.attributes.error_attributes import ( - ERROR_TYPE -) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, From 9f0ae9aff8969d33a57de12de4ffa8e882c1574a Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 10:57:51 -0700 Subject: [PATCH 17/19] Update test_wsgi_middleware.py --- .../tests/test_wsgi_middleware.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index b6b4a13f5d..777d19f41d 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -231,13 +231,10 @@ def validate_response( self.assertEqual(self.status, "200 OK") self.assertEqual(self.response_headers, expected_headers) - expected_attributes = {} if error: self.assertIs(self.exc_info[0], error) self.assertIsInstance(self.exc_info[1], error) self.assertIsNotNone(self.exc_info[2]) - if new_sem_conv: - expected_attributes[ERROR_TYPE] = error else: self.assertIsNone(self.exc_info) @@ -245,6 +242,7 @@ def validate_response( self.assertEqual(len(span_list), 1) self.assertEqual(span_list[0].name, span_name) self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER) + expected_attributes = {} expected_attributes_old = { SpanAttributes.HTTP_SERVER_NAME: "127.0.0.1", SpanAttributes.HTTP_SCHEME: "http", From fef76771c68b4595bf090381de4e13b8d7392e5e Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 11:43:59 -0700 Subject: [PATCH 18/19] tests --- .../tests/test_middleware.py | 2 +- .../tests/test_middleware_asgi.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 364b4bcada..e2a58943e5 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -812,8 +812,8 @@ def test_wsgi_metrics_both_semconv(self): for _ in range(3): response = Client().get("/span_name/1234/") self.assertEqual(response.status_code, 200) - duration = max(round((default_timer() - start) * 1000), 0) duration_s = max(default_timer() - start, 0) + duration = max(round(duration_s * 1000), 0) metrics_list = self.memory_metrics_reader.get_metrics_data() number_data_point_seen = False histrogram_data_point_seen = False diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index 53890b744e..d06c9c635c 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -46,6 +46,7 @@ ) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, + HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, HTTP_ROUTE, ) @@ -520,6 +521,35 @@ async def test_nonstandard_http_method_span_name(self): span = span_list[0] self.assertEqual(span.name, "HTTP") + async def test_nonstandard_http_method_span_name_new_semconv(self): + await self.async_client.request( + method="NONSTANDARD", path="/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, "HTTP") + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") + self.assertEqual( + span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD" + ) + + async def test_nonstandard_http_method_span_name_both_semconv(self): + await self.async_client.request( + method="NONSTANDARD", path="/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, "HTTP") + self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "_OTHER") + self.assertEqual(span.attributes[HTTP_REQUEST_METHOD], "_OTHER") + self.assertEqual( + span.attributes[HTTP_REQUEST_METHOD_ORIGINAL], "NONSTANDARD" + ) + 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() From c4c9d31af16806a03125905e05bc4bb1380cbbba Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 11:50:21 -0700 Subject: [PATCH 19/19] Update test_middleware.py --- .../tests/test_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index e2a58943e5..1b9a904ce1 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -782,7 +782,7 @@ def test_wsgi_metrics_new_semconv(self): self.assertEqual(point.count, 3) histrogram_data_point_seen = True self.assertAlmostEqual( - duration_s, point.sum, places=2 + duration_s, point.sum, places=1 ) if isinstance(point, NumberDataPoint): number_data_point_seen = True