diff --git a/.changelog/5668.fixed b/.changelog/5668.fixed new file mode 100644 index 00000000000..d61b3fa76c9 --- /dev/null +++ b/.changelog/5668.fixed @@ -0,0 +1 @@ +`opentelemetry-exporter-otlp-proto-grpc`: Fix incorrect default port for OTLP gRPC exporter self-metrics diff --git a/docs/getting_started/tests/requirements.txt b/docs/getting_started/tests/requirements.txt index aa9e5ab7ca0..9c82a82acdb 100644 --- a/docs/getting_started/tests/requirements.txt +++ b/docs/getting_started/tests/requirements.txt @@ -22,6 +22,7 @@ wrapt==1.15.0 -e opentelemetry-semantic-conventions -e opentelemetry-proto -e exporter/opentelemetry-exporter-otlp-proto-common +-e exporter/opentelemetry-exporter-otlp-common -e exporter/opentelemetry-exporter-otlp-proto-grpc -e opentelemetry-api -e opentelemetry-sdk diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/_exporter_metrics.py similarity index 85% rename from exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py rename to exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/_exporter_metrics.py index 1524e1dc932..8d51674ef47 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_exporter_metrics.py +++ b/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/_exporter_metrics.py @@ -9,6 +9,7 @@ from dataclasses import dataclass from time import perf_counter from typing import TYPE_CHECKING, Protocol +from urllib.parse import urlparse from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.semconv._incubating.attributes.otel_attributes import ( @@ -33,12 +34,19 @@ if TYPE_CHECKING: from typing import Literal - from urllib.parse import ParseResult as UrlParseResult from opentelemetry.util.types import AnyValue, Attributes _component_counter = Counter() +_GRPC_COMPONENT_TYPES = frozenset( + { + OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, + OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, + OtelComponentTypeValues.OTLP_GRPC_METRIC_EXPORTER, + } +) + @dataclass class ExportResult: @@ -52,6 +60,7 @@ def export_operation(self, num_items: int) -> AbstractContextManager[ExportResul class NoOpExporterMetrics: @contextmanager + # pylint: disable-next=no-self-use def export_operation(self, num_items: int) -> Iterator[ExportResult]: yield ExportResult() @@ -61,7 +70,7 @@ def __init__( self, component_type: OtelComponentTypeValues | None, signal: Literal["traces", "metrics", "logs"], - endpoint: UrlParseResult, + endpoint: str, meter_provider: MeterProvider | None, ) -> None: if signal == "traces": @@ -74,11 +83,15 @@ def __init__( create_exported = create_otel_sdk_exporter_metric_data_point_exported create_inflight = create_otel_sdk_exporter_metric_data_point_inflight - port = endpoint.port + if not endpoint.startswith("//") and "://" not in endpoint: + endpoint = f"//{endpoint}" + parsed_endpoint = urlparse(endpoint) + + port = parsed_endpoint.port if port is None: - if endpoint.scheme == "https": + if component_type in _GRPC_COMPONENT_TYPES or parsed_endpoint.scheme == "https": port = 443 - elif endpoint.scheme == "http": + elif parsed_endpoint.scheme == "http": port = 80 component_type_value = component_type.value if component_type else "unknown_otlp_exporter" @@ -88,8 +101,8 @@ def __init__( OTEL_COMPONENT_TYPE: component_type_value, OTEL_COMPONENT_NAME: f"{component_type_value}/{count}", } - if endpoint.hostname: - self._standard_attrs[SERVER_ADDRESS] = endpoint.hostname + if parsed_endpoint.hostname: + self._standard_attrs[SERVER_ADDRESS] = parsed_endpoint.hostname if port is not None: self._standard_attrs[SERVER_PORT] = port @@ -124,7 +137,7 @@ def export_operation(self, num_items: int) -> Iterator[ExportResult]: def create_exporter_metrics( component_type: OtelComponentTypeValues | None, signal: Literal["traces", "metrics", "logs"], - endpoint: UrlParseResult, + endpoint: str, meter_provider: MeterProvider | None, enabled: bool, ) -> ExporterMetricsT: diff --git a/exporter/opentelemetry-exporter-otlp-common/tests/test_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-common/tests/test_exporter_metrics.py new file mode 100644 index 00000000000..f4830e54e81 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-common/tests/test_exporter_metrics.py @@ -0,0 +1,122 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import unittest +from unittest.mock import Mock, patch + +from opentelemetry.exporter.otlp.common._exporter_metrics import ( + ExporterMetrics, + NoOpExporterMetrics, + create_exporter_metrics, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) + + +class TestExporterMetrics(unittest.TestCase): + def test_factory_returns_noop_when_disabled(self): + meter_provider = Mock() + + with patch("opentelemetry.exporter.otlp.common._exporter_metrics.get_meter_provider") as get_meter_provider: + metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, + "traces", + "http://localhost:4318/v1/traces", + meter_provider, + False, + ) + + self.assertIsInstance(metrics, NoOpExporterMetrics) + meter_provider.get_meter.assert_not_called() + get_meter_provider.assert_not_called() + + def test_factory_returns_exporter_metrics_when_enabled(self): + meter_provider = Mock() + meter_provider.get_meter.return_value = Mock() + + metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, + "traces", + "http://localhost:4318/v1/traces", + meter_provider, + True, + ) + + self.assertIsInstance(metrics, ExporterMetrics) + meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk") + + def test_noop_export_operation_yields_result(self): + metrics = NoOpExporterMetrics() + + with metrics.export_operation(1) as result: + result.error = RuntimeError("error") + + self.assertIsInstance(result.error, RuntimeError) + + def test_grpc_uses_static_port(self): + meter_provider = Mock() + meter_provider.get_meter.return_value = Mock() + + cases: list[tuple[OtelComponentTypeValues, str, str, int]] = [ + ( + OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, + "http://otlp.example.com", + "otlp.example.com", + 443, + ), + ( + OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, + "https://otlp.example.com", + "otlp.example.com", + 443, + ), + ( + OtelComponentTypeValues.OTLP_GRPC_METRIC_EXPORTER, + "otlp.example.com:4317", + "otlp.example.com", + 4317, + ), + ( + OtelComponentTypeValues.OTLP_GRPC_SPAN_EXPORTER, + "http://localhost:4317", + "localhost", + 4317, + ), + ] + + for component_type, endpoint, expected_address, expected_port in cases: + with self.subTest(component_type=component_type, endpoint=endpoint): + metrics = ExporterMetrics(component_type, "traces", endpoint, meter_provider) + # pylint: disable-next=protected-access + self.assertEqual(metrics._standard_attrs[SERVER_ADDRESS], expected_address) + # pylint: disable-next=protected-access + self.assertEqual(metrics._standard_attrs[SERVER_PORT], expected_port) + + def test_http_port_defaults_by_scheme(self): + meter_provider = Mock() + meter_provider.get_meter.return_value = Mock() + + cases: list[tuple[str, int]] = [ + ("http://otlp.example.com", 80), + ("https://otlp.example.com", 443), + ] + + for endpoint, expected_port in cases: + with self.subTest(endpoint=endpoint): + metrics = ExporterMetrics( + OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, + "traces", + endpoint, + meter_provider, + ) + # pylint: disable-next=protected-access + self.assertEqual(metrics._standard_attrs[SERVER_ADDRESS], "otlp.example.com") + # pylint: disable-next=protected-access + self.assertEqual(metrics._standard_attrs[SERVER_PORT], expected_port) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py deleted file mode 100644 index 6f61dc0edea..00000000000 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_exporter_metrics.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -import unittest -from unittest.mock import Mock, patch -from urllib.parse import urlparse - -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( - ExporterMetrics, - NoOpExporterMetrics, - create_exporter_metrics, -) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) - - -class TestExporterMetrics(unittest.TestCase): - def test_factory_returns_noop_when_disabled(self): - meter_provider = Mock() - - with patch( - "opentelemetry.exporter.otlp.proto.common._exporter_metrics.get_meter_provider" - ) as get_meter_provider: - metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, - "traces", - urlparse("http://localhost:4318/v1/traces"), - meter_provider, - False, - ) - - self.assertIsInstance(metrics, NoOpExporterMetrics) - meter_provider.get_meter.assert_not_called() - get_meter_provider.assert_not_called() - - def test_factory_returns_exporter_metrics_when_enabled(self): - meter_provider = Mock() - meter_provider.get_meter.return_value = Mock() - - metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, - "traces", - urlparse("http://localhost:4318/v1/traces"), - meter_provider, - True, - ) - - self.assertIsInstance(metrics, ExporterMetrics) - meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk") - - def test_noop_export_operation_yields_result(self): - metrics = NoOpExporterMetrics() - - with metrics.export_operation(1) as result: - result.error = RuntimeError("error") - - self.assertIsInstance(result.error, RuntimeError) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 8fa96cdcf80..c4906d97690 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "opentelemetry-proto == 1.45.0.dev", "opentelemetry-sdk ~= 1.45.0.dev", "opentelemetry-exporter-otlp-proto-common == 1.45.0.dev", + "opentelemetry-exporter-otlp-common == 0.66b0.dev", "typing-extensions >= 4.6.0", ] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py index f898153dee9..175bd04d653 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/exporter.py @@ -44,7 +44,7 @@ secure_channel, ssl_channel_credentials, ) -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( +from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._internal import ( @@ -358,11 +358,10 @@ def __init__( self._component_type = component_type self._signal: Literal["traces", "metrics", "logs"] = signal - self._parsed_url = parsed_url self._metrics = create_exporter_metrics( self._component_type, signal, - parsed_url, + self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) @@ -518,7 +517,7 @@ def _set_meter_provider(self, meter_provider: MeterProvider) -> None: self._metrics = create_exporter_metrics( self._component_type, self._signal, - self._parsed_url, + self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in b/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in index ca9fa054f9c..45cd7b8ce55 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in @@ -7,6 +7,7 @@ pytest>=9.0.3 -e opentelemetry-api -e tests/opentelemetry-test-utils -e exporter/opentelemetry-exporter-otlp-proto-common +-e exporter/opentelemetry-exporter-otlp-common -e opentelemetry-proto -e opentelemetry-sdk -e opentelemetry-semantic-conventions diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.latest.txt b/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.latest.txt index d7d668f5a13..fea71187ed5 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.latest.txt +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.latest.txt @@ -1,5 +1,9 @@ # This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --constraints=dev-requirements.txt exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in --output-file=exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.latest.txt +-e exporter/opentelemetry-exporter-otlp-common + # via + # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in + # opentelemetry-exporter-otlp-proto-grpc -e exporter/opentelemetry-exporter-otlp-proto-common # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in @@ -21,6 +25,7 @@ -e opentelemetry-sdk # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in + # opentelemetry-exporter-otlp-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-test-utils -e opentelemetry-semantic-conventions diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.oldest.txt b/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.oldest.txt index 0f0f6147f77..68793194e89 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.oldest.txt +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.oldest.txt @@ -1,5 +1,9 @@ # This file was autogenerated by uv via the following command: # uv pip compile --python-version=3.10 --universal --resolution=lowest --constraints=dev-requirements.txt exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in --output-file=exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.oldest.txt +-e exporter/opentelemetry-exporter-otlp-common + # via + # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in + # opentelemetry-exporter-otlp-proto-grpc -e exporter/opentelemetry-exporter-otlp-proto-common # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in @@ -21,6 +25,7 @@ -e opentelemetry-sdk # via # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in + # opentelemetry-exporter-otlp-common # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-test-utils -e opentelemetry-semantic-conventions @@ -33,7 +38,7 @@ asgiref==3.7.2 # via # -c dev-requirements.txt # opentelemetry-test-utils -certifi==2017.4.17 +certifi==2023.5.7 # via requests charset-normalizer==2.0.0 # via requests @@ -76,7 +81,7 @@ pytest==9.0.3 # via # -c dev-requirements.txt # -r exporter/opentelemetry-exporter-otlp-proto-grpc/test-requirements.in -requests==2.32.3 +requests==2.33.0 # via # -c dev-requirements.txt # opentelemetry-test-utils @@ -96,5 +101,5 @@ typing-extensions==4.12.0 ; python_full_version >= '3.14' # opentelemetry-exporter-otlp-proto-grpc # opentelemetry-sdk # opentelemetry-semantic-conventions -urllib3==1.21.1 +urllib3==1.26.0 # via requests diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index bbc707267f3..247855233ad 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -7,10 +7,9 @@ import os from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, overload -from urllib.parse import urlparse from opentelemetry.exporter.otlp.common import http as _http -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( +from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs @@ -167,7 +166,7 @@ def __init__( self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, "logs", - urlparse(self._endpoint), + self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py index 942fc0417ee..f26e29e7a22 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/metric_exporter/__init__.py @@ -11,7 +11,6 @@ Optional, overload, ) -from urllib.parse import urlparse from typing_extensions import deprecated @@ -20,7 +19,7 @@ _get_aggregation, _get_temporality, ) -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( +from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common._internal import ( @@ -224,7 +223,7 @@ def __init__( self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, "metrics", - urlparse(self._endpoint), + self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) @@ -302,7 +301,7 @@ def set_meter_provider(self, meter_provider: MeterProvider) -> None: self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_METRIC_EXPORTER, "metrics", - urlparse(self._endpoint), + self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py index 7da717ea1e5..d4875363bb2 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/trace_exporter/__init__.py @@ -7,10 +7,9 @@ import os from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, overload -from urllib.parse import urlparse from opentelemetry.exporter.otlp.common import http as _http -from opentelemetry.exporter.otlp.proto.common._exporter_metrics import ( +from opentelemetry.exporter.otlp.common._exporter_metrics import ( create_exporter_metrics, ) from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( @@ -163,7 +162,7 @@ def __init__( self._metrics = create_exporter_metrics( OtelComponentTypeValues.OTLP_HTTP_SPAN_EXPORTER, "traces", - urlparse(self._endpoint), + self._endpoint, meter_provider, os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", ) diff --git a/uv.lock b/uv.lock index 98ca024ee9d..c656861a683 100644 --- a/uv.lock +++ b/uv.lock @@ -995,6 +995,7 @@ dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, @@ -1014,6 +1015,7 @@ requires-dist = [ { name = "grpcio", marker = "python_full_version >= '3.14'", specifier = ">=1.75.1,<2.0.0" }, { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-exporter-credential-provider-gcp", marker = "extra == 'gcp-auth'", specifier = ">=0.59b0" }, + { name = "opentelemetry-exporter-otlp-common", editable = "exporter/opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common", editable = "exporter/opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto", editable = "opentelemetry-proto" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" },