From 910d5ec2810294b79e5686af1a13862acc2609d5 Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Mon, 22 Jul 2024 12:02:35 -0700 Subject: [PATCH] HTTP semantic convention stability migration for django (#2714) --- CHANGELOG.md | 6 +- .../aiohttp_server/__init__.py | 2 +- .../instrumentation/django/__init__.py | 49 ++- .../django/middleware/otel_middleware.py | 134 ++++--- .../tests/test_middleware.py | 343 +++++++++++++++++- .../tests/test_middleware_asgi.py | 267 +++++++++++++- .../instrumentation/falcon/__init__.py | 2 +- .../instrumentation/pyramid/callbacks.py | 2 +- .../instrumentation/tornado/__init__.py | 2 +- 9 files changed, 736 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed29f8ad3..a320a1d4fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,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 + ([#2714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2714)) ### Breaking changes @@ -46,8 +48,10 @@ 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 + ([#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-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..651df12043 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): + _DjangoMiddleware._duration_histogram_new = 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..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 @@ -22,6 +22,17 @@ 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, + _HTTPStabilityMode, + _report_new, + _report_old, + _server_active_requests_count_attrs_new, + _server_active_requests_count_attrs_old, + _server_duration_attrs_new, + _server_duration_attrs_old, +) from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) @@ -40,6 +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.trace import SpanAttributes from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import ( @@ -47,13 +59,12 @@ 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, normalise_request_header_name, normalise_response_header_name, + sanitize_method, ) try: @@ -113,26 +124,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 +150,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] = ( @@ -169,6 +162,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 @@ -176,10 +172,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 @@ -213,7 +209,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), @@ -226,14 +225,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 +309,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 +349,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 +384,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 +421,46 @@ 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 - ) - self._duration_histogram.record(duration, duration_attrs) + 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 + ) 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-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 4945f05455..1b9a904ce1 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 @@ -24,6 +25,14 @@ 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, + _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 +48,23 @@ ) 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, +) +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.wsgitestutil import WsgiTestBase from opentelemetry.trace import ( @@ -51,8 +77,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 +136,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"), @@ -199,6 +233,57 @@ def test_traced_get(self): 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() @@ -245,6 +330,53 @@ def test_traced_post(self): 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/") @@ -279,6 +411,67 @@ def test_error(self): 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") + self.assertEqual(span.attributes[ERROR_TYPE], "500") + + 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") + self.assertEqual(span.attributes[ERROR_TYPE], "500") + def test_exclude_lists(self): client = Client() client.get("/excluded_arg/123") @@ -343,6 +536,46 @@ def test_span_name_404(self): 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, "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") span_list = self.memory_exporter.get_finished_spans() @@ -479,9 +712,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,6 +749,105 @@ def test_wsgi_metrics(self): ) self.assertTrue(histrogram_data_point_seen and number_data_point_seen) + # 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_s = 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: + 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 + self.assertAlmostEqual( + duration_s, point.sum, places=1 + ) + 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) + + # pylint: disable=too-many-locals + # pylint: disable=too-many-nested-blocks + 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_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 + + 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=1 + ) + elif metric.name == "http.server.duration": + self.assertAlmostEqual( + duration, point.sum, delta=100 + ) + 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_wsgi_metrics_unistrument(self): Client().get("/span_name/1234/") _django_instrumentor.uninstrument() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index 0e2472d15e..d06c9c635c 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,22 @@ 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.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, +) +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 ( @@ -87,6 +107,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): @@ -96,15 +117,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 +183,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 +256,51 @@ 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 +336,51 @@ 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/") @@ -240,6 +412,60 @@ async def test_error(self): 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") span_list = self.memory_exporter.get_finished_spans() @@ -285,6 +511,45 @@ 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_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() 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,