diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64c39745f2..4eba781aff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: - 'release/*' pull_request: env: - CORE_REPO_SHA: 9831afaff5b4d371fd9a14266ab47884546bd971 + CORE_REPO_SHA: 35a021194787359324c46f5ca99d31802e4c92bd jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 192f2ba373..ff63814aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,13 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1987](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1987)) - `opentelemetry-instrumentation-httpx` Fix mixing async and non async hooks ([#1920](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1920)) +- `opentelemetry-instrumentation-requests` Implement new semantic convention opt-in with stable http semantic conventions + ([#2002](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2002)) - `opentelemetry-instrument-grpc` Fix arity of context.abort for AIO RPCs ([#2066](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2066)) ### Fixed - `opentelemetry-instrumentation-urllib`/`opentelemetry-instrumentation-urllib3` Fix metric descriptions to match semantic conventions - ([#1959]((https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959)) + ([#1959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959)) ## Version 1.21.0/0.42b0 (2023-11-01) diff --git a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py index 535b14285f..0a8e4ee729 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-requests/src/opentelemetry/instrumentation/requests/__init__.py @@ -62,6 +62,27 @@ # FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from opentelemetry.instrumentation._semconv import ( + _METRIC_ATTRIBUTES_CLIENT_DURATION_NAME, + _SPAN_ATTRIBUTES_ERROR_TYPE, + _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS, + _SPAN_ATTRIBUTES_NETWORK_PEER_PORT, + _filter_duration_attrs, + _get_schema_url, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilityMode, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, + _set_http_hostname, + _set_http_method, + _set_http_net_peer_name, + _set_http_network_protocol_version, + _set_http_port, + _set_http_scheme, + _set_http_status_code, + _set_http_url, +) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.requests.package import _instruments from opentelemetry.instrumentation.requests.version import __version__ @@ -72,15 +93,15 @@ from opentelemetry.metrics import Histogram, get_meter from opentelemetry.propagate import inject from opentelemetry.semconv.metrics import MetricInstruments -from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind, Tracer, get_tracer from opentelemetry.trace.span import Span -from opentelemetry.trace.status import Status +from opentelemetry.trace.status import StatusCode from opentelemetry.util.http import ( ExcludeList, get_excluded_urls, parse_excluded_urls, remove_url_credentials, + sanitize_method, ) from opentelemetry.util.http.httplib import set_ip_on_next_http_connection @@ -94,10 +115,12 @@ # pylint: disable=R0915 def _instrument( tracer: Tracer, - duration_histogram: Histogram, + duration_histogram_old: Histogram, + duration_histogram_new: Histogram, request_hook: _RequestHookT = None, response_hook: _ResponseHookT = None, excluded_urls: ExcludeList = None, + sem_conv_opt_in_mode: _OpenTelemetryStabilityMode = _OpenTelemetryStabilityMode.DEFAULT, ): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes @@ -132,31 +155,58 @@ def get_or_create_headers(): return wrapped_send(self, request, **kwargs) # See - # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-client - method = request.method.upper() + # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client + method = request.method span_name = get_default_span_name(method) url = remove_url_credentials(request.url) - span_attributes = { - SpanAttributes.HTTP_METHOD: method, - SpanAttributes.HTTP_URL: url, - } + span_attributes = {} + _set_http_method( + span_attributes, method, span_name, sem_conv_opt_in_mode + ) + _set_http_url(span_attributes, url, sem_conv_opt_in_mode) - metric_labels = { - SpanAttributes.HTTP_METHOD: method, - } + metric_labels = {} + _set_http_method( + metric_labels, method, span_name, sem_conv_opt_in_mode + ) try: parsed_url = urlparse(url) - metric_labels[SpanAttributes.HTTP_SCHEME] = parsed_url.scheme + if parsed_url.scheme: + _set_http_scheme( + metric_labels, parsed_url.scheme, sem_conv_opt_in_mode + ) if parsed_url.hostname: - metric_labels[SpanAttributes.HTTP_HOST] = parsed_url.hostname - metric_labels[ - SpanAttributes.NET_PEER_NAME - ] = parsed_url.hostname + _set_http_hostname( + metric_labels, parsed_url.hostname, sem_conv_opt_in_mode + ) + _set_http_net_peer_name( + metric_labels, parsed_url.hostname, sem_conv_opt_in_mode + ) + if _report_new(sem_conv_opt_in_mode): + _set_http_hostname( + span_attributes, + parsed_url.hostname, + sem_conv_opt_in_mode, + ) + # Use semconv library when available + span_attributes[ + _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS + ] = parsed_url.hostname if parsed_url.port: - metric_labels[SpanAttributes.NET_PEER_PORT] = parsed_url.port + _set_http_port( + metric_labels, parsed_url.port, sem_conv_opt_in_mode + ) + if _report_new(sem_conv_opt_in_mode): + _set_http_port( + span_attributes, parsed_url.port, sem_conv_opt_in_mode + ) + # Use semconv library when available + span_attributes[ + _SPAN_ATTRIBUTES_NETWORK_PEER_PORT + ] = parsed_url.port except ValueError: pass @@ -182,35 +232,78 @@ def get_or_create_headers(): exception = exc result = getattr(exc, "response", None) finally: - elapsed_time = max( - round((default_timer() - start_time) * 1000), 0 - ) + elapsed_time = max(default_timer() - start_time, 0) context.detach(token) if isinstance(result, Response): + span_attributes = {} if span.is_recording(): - span.set_attribute( - SpanAttributes.HTTP_STATUS_CODE, result.status_code + _set_http_status_code( + span_attributes, + result.status_code, + sem_conv_opt_in_mode, ) - span.set_status( - Status(http_status_to_status_code(result.status_code)) + _set_http_status_code( + metric_labels, result.status_code, sem_conv_opt_in_mode ) - - metric_labels[ - SpanAttributes.HTTP_STATUS_CODE - ] = result.status_code + status_code = http_status_to_status_code( + result.status_code + ) + span.set_status(status_code) + if ( + _report_new(sem_conv_opt_in_mode) + and status_code is StatusCode.ERROR + ): + span_attributes[_SPAN_ATTRIBUTES_ERROR_TYPE] = str( + result.status_code + ) + metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = str( + result.status_code + ) if result.raw is not None: version = getattr(result.raw, "version", None) if version: - metric_labels[SpanAttributes.HTTP_FLAVOR] = ( - "1.1" if version == 11 else "1.0" + # Only HTTP/1 is supported by requests + version_text = "1.1" if version == 11 else "1.0" + _set_http_network_protocol_version( + metric_labels, version_text, sem_conv_opt_in_mode ) + if _report_new(sem_conv_opt_in_mode): + _set_http_network_protocol_version( + span_attributes, + version_text, + sem_conv_opt_in_mode, + ) + for key, val in span_attributes.items(): + span.set_attribute(key, val) if callable(response_hook): response_hook(span, request, result) - duration_histogram.record(elapsed_time, attributes=metric_labels) + if exception is not None and _report_new(sem_conv_opt_in_mode): + span.set_attribute( + _SPAN_ATTRIBUTES_ERROR_TYPE, type(exception).__qualname__ + ) + metric_labels[_SPAN_ATTRIBUTES_ERROR_TYPE] = type( + exception + ).__qualname__ + + if duration_histogram_old is not None: + duration_attrs_old = _filter_duration_attrs( + metric_labels, _OpenTelemetryStabilityMode.DEFAULT + ) + duration_histogram_old.record( + max(round(elapsed_time * 1000), 0), + attributes=duration_attrs_old, + ) + if duration_histogram_new is not None: + duration_attrs_new = _filter_duration_attrs( + metric_labels, _OpenTelemetryStabilityMode.HTTP + ) + duration_histogram_new.record( + elapsed_time, attributes=duration_attrs_new + ) if exception is not None: raise exception.with_traceback(exception.__traceback__) @@ -254,7 +347,7 @@ def get_default_span_name(method): Returns: span name """ - return method.strip() + return sanitize_method(method.upper().strip()) class RequestsInstrumentor(BaseInstrumentor): @@ -276,12 +369,16 @@ def _instrument(self, **kwargs): ``excluded_urls``: A string containing a comma-delimited list of regexes used to exclude URLs from tracking """ + semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + schema_url = _get_schema_url(semconv_opt_in_mode) tracer_provider = kwargs.get("tracer_provider") tracer = get_tracer( __name__, __version__, tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=schema_url, ) excluded_urls = kwargs.get("excluded_urls") meter_provider = kwargs.get("meter_provider") @@ -289,21 +386,32 @@ def _instrument(self, **kwargs): __name__, __version__, meter_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", - ) - duration_histogram = meter.create_histogram( - name=MetricInstruments.HTTP_CLIENT_DURATION, - unit="ms", - description="measures the duration of the outbound HTTP request", + schema_url=schema_url, ) + duration_histogram_old = None + if _report_old(semconv_opt_in_mode): + duration_histogram_old = meter.create_histogram( + name=MetricInstruments.HTTP_CLIENT_DURATION, + unit="ms", + description="measures the duration of the outbound HTTP request", + ) + duration_histogram_new = None + if _report_new(semconv_opt_in_mode): + duration_histogram_new = meter.create_histogram( + name=_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME, + unit="s", + description="Duration of HTTP client requests.", + ) _instrument( tracer, - duration_histogram, + duration_histogram_old, + duration_histogram_new, request_hook=kwargs.get("request_hook"), response_hook=kwargs.get("response_hook"), excluded_urls=_excluded_urls_from_env if excluded_urls is None else parse_excluded_urls(excluded_urls), + sem_conv_opt_in_mode=semconv_opt_in_mode, ) def _uninstrument(self, **kwargs): diff --git a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py index 82fa4ed1e6..11eada2589 100644 --- a/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py +++ b/instrumentation/opentelemetry-instrumentation-requests/tests/test_requests_integration.py @@ -25,6 +25,13 @@ # FIXME: fix the importing of this private attribute when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined. from opentelemetry.context import _SUPPRESS_HTTP_INSTRUMENTATION_KEY +from opentelemetry.instrumentation._semconv import ( + _OTEL_SEMCONV_STABILITY_OPT_IN_KEY, + _SPAN_ATTRIBUTES_ERROR_TYPE, + _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS, + _SPAN_ATTRIBUTES_NETWORK_PEER_PORT, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY from opentelemetry.propagate import get_global_textmap, set_global_textmap @@ -69,12 +76,24 @@ class RequestsIntegrationTestBase(abc.ABC): def setUp(self): super().setUp() + 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 = mock.patch.dict( "os.environ", { - "OTEL_PYTHON_REQUESTS_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg" + "OTEL_PYTHON_REQUESTS_EXCLUDED_URLS": "http://localhost/env_excluded_arg/123,env_excluded_noarg", + _OTEL_SEMCONV_STABILITY_OPT_IN_KEY: sem_conv_mode, }, ) + + _OpenTelemetrySemanticConventionStability._initialized = False + self.env_patch.start() self.exclude_patch = mock.patch( @@ -118,6 +137,11 @@ def test_basic(self): self.assertIs(span.kind, trace.SpanKind.CLIENT) self.assertEqual(span.name, "GET") + self.assertEqual( + span.instrumentation_scope.schema_url, + "https://opentelemetry.io/schemas/1.11.0", + ) + self.assertEqual( span.attributes, { @@ -133,6 +157,84 @@ def test_basic(self): span, opentelemetry.instrumentation.requests ) + def test_basic_new_semconv(self): + url_with_port = "http://mock:80/status/200" + httpretty.register_uri( + httpretty.GET, url_with_port, status=200, body="Hello!" + ) + result = self.perform_request(url_with_port) + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + + self.assertIs(span.kind, trace.SpanKind.CLIENT) + self.assertEqual(span.name, "GET") + + self.assertEqual( + span.instrumentation_scope.schema_url, + SpanAttributes.SCHEMA_URL, + ) + self.assertEqual( + span.attributes, + { + SpanAttributes.HTTP_REQUEST_METHOD: "GET", + SpanAttributes.URL_FULL: url_with_port, + SpanAttributes.SERVER_ADDRESS: "mock", + _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + SpanAttributes.SERVER_PORT: 80, + _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, + }, + ) + + self.assertIs(span.status.status_code, trace.StatusCode.UNSET) + + self.assertEqualSpanInstrumentationScope( + span, opentelemetry.instrumentation.requests + ) + + def test_basic_both_semconv(self): + url_with_port = "http://mock:80/status/200" + httpretty.register_uri( + httpretty.GET, url_with_port, status=200, body="Hello!" + ) + result = self.perform_request(url_with_port) + self.assertEqual(result.text, "Hello!") + span = self.assert_span() + + self.assertIs(span.kind, trace.SpanKind.CLIENT) + self.assertEqual(span.name, "GET") + + self.assertEqual( + span.instrumentation_scope.schema_url, + SpanAttributes.SCHEMA_URL, + ) + self.assertEqual( + span.attributes, + { + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_REQUEST_METHOD: "GET", + SpanAttributes.HTTP_URL: url_with_port, + SpanAttributes.URL_FULL: url_with_port, + SpanAttributes.HTTP_HOST: "mock", + SpanAttributes.SERVER_ADDRESS: "mock", + _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", + SpanAttributes.NET_PEER_PORT: 80, + SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + SpanAttributes.SERVER_PORT: 80, + _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, + }, + ) + + self.assertIs(span.status.status_code, trace.StatusCode.UNSET) + + self.assertEqualSpanInstrumentationScope( + span, opentelemetry.instrumentation.requests + ) + def test_hooks(self): def request_hook(span, request_obj): span.update_name("name set from hook") @@ -214,6 +316,57 @@ def test_not_foundbasic(self): trace.StatusCode.ERROR, ) + def test_not_foundbasic_new_semconv(self): + url_404 = "http://mock/status/404" + httpretty.register_uri( + httpretty.GET, + url_404, + status=404, + ) + result = self.perform_request(url_404) + self.assertEqual(result.status_code, 404) + + span = self.assert_span() + + self.assertEqual( + span.attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404 + ) + self.assertEqual( + span.attributes.get(_SPAN_ATTRIBUTES_ERROR_TYPE), "404" + ) + + self.assertIs( + span.status.status_code, + trace.StatusCode.ERROR, + ) + + def test_not_foundbasic_both_semconv(self): + url_404 = "http://mock/status/404" + httpretty.register_uri( + httpretty.GET, + url_404, + status=404, + ) + result = self.perform_request(url_404) + self.assertEqual(result.status_code, 404) + + span = self.assert_span() + + self.assertEqual( + span.attributes.get(SpanAttributes.HTTP_STATUS_CODE), 404 + ) + self.assertEqual( + span.attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE), 404 + ) + self.assertEqual( + span.attributes.get(_SPAN_ATTRIBUTES_ERROR_TYPE), "404" + ) + + self.assertIs( + span.status.status_code, + trace.StatusCode.ERROR, + ) + def test_uninstrument(self): RequestsInstrumentor().uninstrument() result = self.perform_request(self.URL) @@ -368,6 +521,34 @@ def test_requests_exception_without_response(self, *_, **__): ) self.assertEqual(span.status.status_code, StatusCode.ERROR) + @mock.patch( + "requests.adapters.HTTPAdapter.send", + side_effect=requests.RequestException, + ) + def test_requests_exception_new_semconv(self, *_, **__): + url_with_port = "http://mock:80/status/200" + httpretty.register_uri( + httpretty.GET, url_with_port, status=200, body="Hello!" + ) + with self.assertRaises(requests.RequestException): + self.perform_request(url_with_port) + + span = self.assert_span() + print(span.attributes) + self.assertEqual( + span.attributes, + { + SpanAttributes.HTTP_REQUEST_METHOD: "GET", + SpanAttributes.URL_FULL: url_with_port, + SpanAttributes.SERVER_ADDRESS: "mock", + SpanAttributes.SERVER_PORT: 80, + _SPAN_ATTRIBUTES_NETWORK_PEER_PORT: 80, + _SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS: "mock", + _SPAN_ATTRIBUTES_ERROR_TYPE: "RequestException", + }, + ) + self.assertEqual(span.status.status_code, StatusCode.ERROR) + mocked_response = requests.Response() mocked_response.status_code = 500 mocked_response.reason = "Internal Server Error" @@ -489,6 +670,22 @@ class TestRequestsIntergrationMetric(TestBase): def setUp(self): super().setUp() + 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 = mock.patch.dict( + "os.environ", + { + _OTEL_SEMCONV_STABILITY_OPT_IN_KEY: sem_conv_mode, + }, + ) + self.env_patch.start() + _OpenTelemetrySemanticConventionStability._initialized = False RequestsInstrumentor().instrument(meter_provider=self.meter_provider) httpretty.enable() @@ -496,6 +693,7 @@ def setUp(self): def tearDown(self): super().tearDown() + self.env_patch.stop() RequestsInstrumentor().uninstrument() httpretty.disable() @@ -507,22 +705,94 @@ def test_basic_metric_success(self): self.perform_request(self.URL) expected_attributes = { - "http.status_code": 200, - "http.host": "examplehost", - "net.peer.port": 8000, - "net.peer.name": "examplehost", - "http.method": "GET", - "http.flavor": "1.1", - "http.scheme": "http", + SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_HOST: "examplehost", + SpanAttributes.NET_PEER_PORT: 8000, + SpanAttributes.NET_PEER_NAME: "examplehost", + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SCHEME: "http", } for ( resource_metrics ) in self.memory_metrics_reader.get_metrics_data().resource_metrics: for scope_metrics in resource_metrics.scope_metrics: + self.assertEqual(len(scope_metrics.metrics), 1) + for metric in scope_metrics.metrics: + self.assertEqual(metric.unit, "ms") + self.assertEqual( + metric.description, + "measures the duration of the outbound HTTP request", + ) + for data_point in metric.data.data_points: + self.assertDictEqual( + expected_attributes, dict(data_point.attributes) + ) + self.assertEqual(data_point.count, 1) + + def test_basic_metric_new_semconv(self): + self.perform_request(self.URL) + + expected_attributes = { + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.SERVER_ADDRESS: "examplehost", + SpanAttributes.SERVER_PORT: 8000, + SpanAttributes.HTTP_REQUEST_METHOD: "GET", + SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + } + for ( + resource_metrics + ) in self.memory_metrics_reader.get_metrics_data().resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + self.assertEqual(len(scope_metrics.metrics), 1) for metric in scope_metrics.metrics: + self.assertEqual(metric.unit, "s") + self.assertEqual( + metric.description, "Duration of HTTP client requests." + ) for data_point in metric.data.data_points: self.assertDictEqual( expected_attributes, dict(data_point.attributes) ) self.assertEqual(data_point.count, 1) + + def test_basic_metric_both_semconv(self): + self.perform_request(self.URL) + + expected_attributes_old = { + SpanAttributes.HTTP_STATUS_CODE: 200, + SpanAttributes.HTTP_HOST: "examplehost", + SpanAttributes.NET_PEER_PORT: 8000, + SpanAttributes.NET_PEER_NAME: "examplehost", + SpanAttributes.HTTP_METHOD: "GET", + SpanAttributes.HTTP_FLAVOR: "1.1", + SpanAttributes.HTTP_SCHEME: "http", + } + + expected_attributes_new = { + SpanAttributes.HTTP_RESPONSE_STATUS_CODE: 200, + SpanAttributes.SERVER_ADDRESS: "examplehost", + SpanAttributes.SERVER_PORT: 8000, + SpanAttributes.HTTP_REQUEST_METHOD: "GET", + SpanAttributes.NETWORK_PROTOCOL_VERSION: "1.1", + } + + for ( + resource_metrics + ) in self.memory_metrics_reader.get_metrics_data().resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + self.assertEqual(len(scope_metrics.metrics), 2) + for metric in scope_metrics.metrics: + for data_point in metric.data.data_points: + if metric.unit == "ms": + self.assertDictEqual( + expected_attributes_old, + dict(data_point.attributes), + ) + else: + self.assertDictEqual( + expected_attributes_new, + dict(data_point.attributes), + ) + self.assertEqual(data_point.count, 1) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py new file mode 100644 index 0000000000..fbfc92cf21 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/_semconv.py @@ -0,0 +1,217 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import threading +from enum import Enum + +from opentelemetry.semconv.trace import SpanAttributes + +# TODO: will come through semconv package once updated +_SPAN_ATTRIBUTES_ERROR_TYPE = "error.type" +_SPAN_ATTRIBUTES_NETWORK_PEER_ADDRESS = "network.peer.address" +_SPAN_ATTRIBUTES_NETWORK_PEER_PORT = "network.peer.port" +_METRIC_ATTRIBUTES_CLIENT_DURATION_NAME = "http.client.request.duration" + +_client_duration_attrs_old = [ + SpanAttributes.HTTP_STATUS_CODE, + SpanAttributes.HTTP_HOST, + SpanAttributes.NET_PEER_PORT, + SpanAttributes.NET_PEER_NAME, + SpanAttributes.HTTP_METHOD, + SpanAttributes.HTTP_FLAVOR, + SpanAttributes.HTTP_SCHEME, +] + +_client_duration_attrs_new = [ + _SPAN_ATTRIBUTES_ERROR_TYPE, + SpanAttributes.HTTP_REQUEST_METHOD, + SpanAttributes.HTTP_RESPONSE_STATUS_CODE, + SpanAttributes.NETWORK_PROTOCOL_VERSION, + SpanAttributes.SERVER_ADDRESS, + SpanAttributes.SERVER_PORT, + # TODO: Support opt-in for scheme in new semconv + # SpanAttributes.URL_SCHEME, +] + + +def _filter_duration_attrs(attrs, sem_conv_opt_in_mode): + filtered_attrs = {} + allowed_attributes = ( + _client_duration_attrs_new + if sem_conv_opt_in_mode == _OpenTelemetryStabilityMode.HTTP + else _client_duration_attrs_old + ) + for key, val in attrs.items(): + if key in allowed_attributes: + filtered_attrs[key] = val + return filtered_attrs + + +def set_string_attribute(result, key, value): + if value: + result[key] = value + + +def set_int_attribute(result, key, value): + if value: + try: + result[key] = int(value) + except ValueError: + return + + +def _set_http_method(result, original, normalized, sem_conv_opt_in_mode): + original = original.strip() + normalized = normalized.strip() + # See https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#common-attributes + # Method is case sensitive. "http.request.method_original" should not be sanitized or automatically capitalized. + if original != normalized and _report_new(sem_conv_opt_in_mode): + set_string_attribute( + result, SpanAttributes.HTTP_REQUEST_METHOD_ORIGINAL, original + ) + + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_METHOD, normalized) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute( + result, SpanAttributes.HTTP_REQUEST_METHOD, normalized + ) + + +def _set_http_url(result, url, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_URL, url) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.URL_FULL, url) + + +def _set_http_scheme(result, scheme, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_SCHEME, scheme) + # TODO: Support opt-in for scheme in new semconv + # if _report_new(sem_conv_opt_in_mode): + # set_string_attribute(result, SpanAttributes.URL_SCHEME, scheme) + + +def _set_http_hostname(result, hostname, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_HOST, hostname) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, hostname) + + +def _set_http_net_peer_name(result, peer_name, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.NET_PEER_NAME, peer_name) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.SERVER_ADDRESS, peer_name) + + +def _set_http_port(result, port, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.NET_PEER_PORT, port) + if _report_new(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.SERVER_PORT, port) + + +def _set_http_status_code(result, code, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_int_attribute(result, SpanAttributes.HTTP_STATUS_CODE, code) + if _report_new(sem_conv_opt_in_mode): + set_int_attribute( + result, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, code + ) + + +def _set_http_network_protocol_version(result, version, sem_conv_opt_in_mode): + if _report_old(sem_conv_opt_in_mode): + set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version) + if _report_new(sem_conv_opt_in_mode): + set_string_attribute( + result, SpanAttributes.NETWORK_PROTOCOL_VERSION, version + ) + + +_OTEL_SEMCONV_STABILITY_OPT_IN_KEY = "OTEL_SEMCONV_STABILITY_OPT_IN" + + +class _OpenTelemetryStabilitySignalType: + HTTP = "http" + + +class _OpenTelemetryStabilityMode(Enum): + # http - emit the new, stable HTTP and networking conventions ONLY + HTTP = "http" + # http/dup - emit both the old and the stable HTTP and networking conventions + HTTP_DUP = "http/dup" + # default - continue emitting old experimental HTTP and networking conventions + DEFAULT = "default" + + +def _report_new(mode): + return mode.name != _OpenTelemetryStabilityMode.DEFAULT.name + + +def _report_old(mode): + return mode.name != _OpenTelemetryStabilityMode.HTTP.name + + +class _OpenTelemetrySemanticConventionStability: + _initialized = False + _lock = threading.Lock() + _OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {} + + @classmethod + def _initialize(cls): + with _OpenTelemetrySemanticConventionStability._lock: + if not _OpenTelemetrySemanticConventionStability._initialized: + # Users can pass in comma delimited string for opt-in options + # Only values for http stability are supported for now + opt_in = os.environ.get(_OTEL_SEMCONV_STABILITY_OPT_IN_KEY, "") + opt_in_list = [] + if opt_in: + opt_in_list = [s.strip() for s in opt_in.split(",")] + http_opt_in = _OpenTelemetryStabilityMode.DEFAULT + if opt_in_list: + # Process http opt-in + # http/dup takes priority over http + if ( + _OpenTelemetryStabilityMode.HTTP_DUP.value + in opt_in_list + ): + http_opt_in = _OpenTelemetryStabilityMode.HTTP_DUP + elif _OpenTelemetryStabilityMode.HTTP.value in opt_in_list: + http_opt_in = _OpenTelemetryStabilityMode.HTTP + _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ + _OpenTelemetryStabilitySignalType.HTTP + ] = http_opt_in + _OpenTelemetrySemanticConventionStability._initialized = True + + @classmethod + # Get OpenTelemetry opt-in mode based off of signal type (http, messaging, etc.) + def _get_opentelemetry_stability_opt_in_mode( + cls, + signal_type: _OpenTelemetryStabilitySignalType, + ) -> _OpenTelemetryStabilityMode: + return _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get( + signal_type, _OpenTelemetryStabilityMode.DEFAULT + ) + + +# Get schema version based off of opt-in mode +def _get_schema_url(mode: _OpenTelemetryStabilityMode) -> str: + if mode is _OpenTelemetryStabilityMode.DEFAULT: + return "https://opentelemetry.io/schemas/1.11.0" + return SpanAttributes.SCHEMA_URL diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py index 6c6f86fd51..c612bfeceb 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py @@ -21,13 +21,13 @@ from logging import getLogger from typing import Collection, Optional +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.dependencies import ( DependencyConflict, get_dependency_conflicts, ) -from opentelemetry.instrumentation.utils import ( - _OpenTelemetrySemanticConventionStability, -) _LOG = getLogger(__name__) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 71a0087149..35a55a1279 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -12,10 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import threading import urllib.parse -from enum import Enum from re import escape, sub from typing import Dict, Sequence @@ -155,61 +152,3 @@ def _python_path_without_directory(python_path, directory, path_separator): "", python_path, ) - - -_OTEL_SEMCONV_STABILITY_OPT_IN_KEY = "OTEL_SEMCONV_STABILITY_OPT_IN" - - -class _OpenTelemetryStabilitySignalType: - HTTP = "http" - - -class _OpenTelemetryStabilityMode(Enum): - # http - emit the new, stable HTTP and networking conventions ONLY - HTTP = "http" - # http/dup - emit both the old and the stable HTTP and networking conventions - HTTP_DUP = "http/dup" - # default - continue emitting old experimental HTTP and networking conventions - DEFAULT = "default" - - -class _OpenTelemetrySemanticConventionStability: - _initialized = False - _lock = threading.Lock() - _OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {} - - @classmethod - def _initialize(cls): - with _OpenTelemetrySemanticConventionStability._lock: - if not _OpenTelemetrySemanticConventionStability._initialized: - # Users can pass in comma delimited string for opt-in options - # Only values for http stability are supported for now - opt_in = os.environ.get(_OTEL_SEMCONV_STABILITY_OPT_IN_KEY, "") - opt_in_list = [] - if opt_in: - opt_in_list = [s.strip() for s in opt_in.split(",")] - http_opt_in = _OpenTelemetryStabilityMode.DEFAULT - if opt_in_list: - # Process http opt-in - # http/dup takes priority over http - if ( - _OpenTelemetryStabilityMode.HTTP_DUP.value - in opt_in_list - ): - http_opt_in = _OpenTelemetryStabilityMode.HTTP_DUP - elif _OpenTelemetryStabilityMode.HTTP.value in opt_in_list: - http_opt_in = _OpenTelemetryStabilityMode.HTTP - _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[ - _OpenTelemetryStabilitySignalType.HTTP - ] = http_opt_in - _OpenTelemetrySemanticConventionStability._initialized = True - - @classmethod - def _get_opentelemetry_stability_opt_in( - cls, - signal_type: _OpenTelemetryStabilitySignalType, - ) -> _OpenTelemetryStabilityMode: - with _OpenTelemetrySemanticConventionStability._lock: - return _OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get( - signal_type, _OpenTelemetryStabilityMode.DEFAULT - )