From 613710161555d13cd271efe5541305c8f3fb5692 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 3 Jul 2026 16:45:33 -0600 Subject: [PATCH] opentelemetry-exporter-otlp-pyproto-http: drop requests, use urllib Replace requests.Session-based transport with stdlib urllib.request/ urllib.error across the trace, log and metric HTTP exporters, sharing a small _post/_build_ssl_context helper in _common.py. The credential- provider entry point's requests.Session type check is commented out (rather than removed) since there is no longer a requests.Session type to validate against; the session= constructor param and its wiring are dropped since there is no longer a Session object for it to populate. --- .../pyproject.toml | 1 - .../otlp/pyproto/http/_common/__init__.py | 64 +++++++++++++---- .../pyproto/http/_log_exporter/__init__.py | 72 ++++++++----------- .../pyproto/http/metric_exporter/__init__.py | 72 ++++++++----------- .../pyproto/http/trace_exporter/__init__.py | 72 ++++++++----------- .../pyproto/http/_common/test___init__.py | 32 +++------ .../http/_log_exporter/test___init__.py | 41 +++++------ .../http/metric_exporter/test___init__.py | 38 ++++------ .../http/trace_exporter/test___init__.py | 43 +++++------ uv.lock | 2 - 10 files changed, 194 insertions(+), 243 deletions(-) diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-pyproto-http/pyproject.toml index 54777a25973..7d226a59a1f 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "opentelemetry-api ~= 1.12", "opentelemetry-sdk ~= 1.12", "opentelemetry-exporter-otlp-pyproto-common == 1.43.0.dev", - "requests ~= 2.7", ] [project.entry-points.opentelemetry_traces_exporter] diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_common/__init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_common/__init__.py index bf772c5859b..f28a2ff5c50 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_common/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_common/__init__.py @@ -1,10 +1,11 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +import ssl from os import environ from typing import Literal - -from requests import Response, Session +from urllib.error import HTTPError +from urllib.request import Request, urlopen from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER, @@ -12,21 +13,54 @@ from opentelemetry.util._importlib_metadata import entry_points -def _is_retryable(resp: Response) -> bool: - if resp.status_code == 408: +def _is_retryable(status_code: int) -> bool: + if status_code == 408: return True - if 500 <= resp.status_code <= 599: + if 500 <= status_code <= 599: return True return False +def _build_ssl_context( + certificate_file: str | bool, + client_cert: str | tuple[str, str | None] | None, +) -> ssl.SSLContext: + context = ssl.create_default_context( + cafile=certificate_file if isinstance(certificate_file, str) else None + ) + if certificate_file is False: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + if client_cert: + certfile, keyfile = ( + client_cert if isinstance(client_cert, tuple) else (client_cert, None) + ) + context.load_cert_chain(certfile, keyfile) + return context + + +def _post( + url: str, + data: bytes, + headers: dict[str, str], + timeout_sec: float, + ssl_context: ssl.SSLContext, +) -> tuple[int, str]: + request = Request(url, data=data, headers=headers, method="POST") + try: + with urlopen(request, timeout=timeout_sec, context=ssl_context) as response: + return response.status, response.reason + except HTTPError as error: + return error.code, error.reason + + def _load_session_from_envvar( cred_envvar: Literal[ "OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER", "OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER", "OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER", ], -) -> Session | None: +): _credential_env = environ.get( _OTEL_PYTHON_EXPORTER_OTLP_HTTP_CREDENTIAL_PROVIDER ) or environ.get(cred_envvar) @@ -45,11 +79,15 @@ def _load_session_from_envvar( f"Requested component '{_credential_env}' not found in " f"entry point 'opentelemetry_otlp_credential_provider'" ) - if isinstance(maybe_session, Session): - return maybe_session - else: - raise RuntimeError( - f"Requested component '{_credential_env}' is of type {type(maybe_session)}" - f" must be of type `Session`." - ) + # `requests` is no longer a dependency of this package, so the + # provider's return value can no longer be verified here. + # from requests import Session + # if isinstance(maybe_session, Session): + # return maybe_session + # else: + # raise RuntimeError( + # f"Requested component '{_credential_env}' is of type {type(maybe_session)}" + # f" must be of type `Session`." + # ) + return maybe_session return None diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/__init__.py index d7fa985d526..1bb46b4d60a 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/__init__.py @@ -10,12 +10,10 @@ from random import uniform from threading import Event from time import time +from urllib.error import URLError from urllib.parse import urlparse from zlib import compress -from requests import Session -from requests.exceptions import ConnectionError, RequestException - from opentelemetry.exporter.otlp.pyproto.common._exporter_metrics import ( create_exporter_metrics, ) @@ -27,8 +25,9 @@ Compression, ) from opentelemetry.exporter.otlp.pyproto.http._common import ( + _build_ssl_context, _is_retryable, - _load_session_from_envvar, + _post, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk._logs import ReadableLogRecord @@ -38,7 +37,6 @@ ) from opentelemetry.sdk._shared_internal import DuplicateFilter from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, @@ -82,7 +80,6 @@ def __init__( headers: dict[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, - session: Session | None = None, *, meter_provider: MeterProvider | None = None, ): @@ -122,20 +119,12 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER - ) - or Session() - ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - self._session.headers.update(self._headers) + self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers} if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) + self._request_headers["Content-Encoding"] = self._compression.value + self._ssl_context = _build_ssl_context( + self._certificate_file, self._client_cert + ) self._shutdown = False self._metrics = create_exporter_metrics( @@ -161,22 +150,21 @@ def _export(self, serialized_data: bytes, timeout_sec: float | None = None): if timeout_sec is None: timeout_sec = self._timeout try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, + return _post( + self._endpoint, + data, + self._request_headers, + timeout_sec, + self._ssl_context, ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, + except URLError: + return _post( + self._endpoint, + data, + self._request_headers, + timeout_sec, + self._ssl_context, ) - return resp def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: if self._shutdown: @@ -190,18 +178,17 @@ def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: backoff_seconds = 2**retry_num * uniform(0.8, 1.2) export_error: Exception | None = None try: - resp = self._export(serialized_data, deadline_sec - time()) - if resp.ok: + status_code, reason = self._export( + serialized_data, deadline_sec - time() + ) + if status_code < 400: return LogRecordExportResult.SUCCESS - except RequestException as error: - reason = error + retryable = _is_retryable(status_code) + except URLError as error: + reason = error.reason export_error = error - retryable = isinstance(error, ConnectionError) + retryable = True status_code = None - else: - reason = resp.reason - retryable = _is_retryable(resp) - status_code = resp.status_code if not retryable: _logger.error( @@ -254,7 +241,6 @@ def shutdown(self): return self._shutdown = True self._shutdown_is_occuring.set() - self._session.close() def _compression_from_env() -> Compression: diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/__init__.py index 3c1938729c7..269e29a3221 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/__init__.py @@ -11,12 +11,10 @@ from random import uniform from threading import Event from time import time +from urllib.error import URLError from urllib.parse import urlparse from zlib import compress -from requests import Session -from requests.exceptions import ConnectionError, RequestException - from opentelemetry.exporter.otlp.pyproto.common._exporter_metrics import ( create_exporter_metrics, ) @@ -29,8 +27,9 @@ Compression, ) from opentelemetry.exporter.otlp.pyproto.http._common import ( + _build_ssl_context, _is_retryable, - _load_session_from_envvar, + _post, ) from opentelemetry.metrics import MeterProvider from opentelemetry.pyproto.collector.metrics.v1.metrics_service_pypb2 import ( @@ -47,7 +46,6 @@ Summary, ) from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, @@ -97,7 +95,6 @@ def __init__( headers: dict[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, - session: Session | None = None, preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[type, Aggregation] | None = None, max_export_batch_size: int | None = None, @@ -140,20 +137,12 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_METRICS_CREDENTIAL_PROVIDER - ) - or Session() - ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - self._session.headers.update(self._headers) + self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers} if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) + self._request_headers["Content-Encoding"] = self._compression.value + self._ssl_context = _build_ssl_context( + self._certificate_file, self._client_cert + ) self._common_configuration(preferred_temporality, preferred_aggregation) self._max_export_batch_size = max_export_batch_size self._shutdown = False @@ -181,22 +170,21 @@ def _export(self, serialized_data: bytes, timeout_sec: float | None = None): if timeout_sec is None: timeout_sec = self._timeout try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, + return _post( + self._endpoint, + data, + self._request_headers, + timeout_sec, + self._ssl_context, ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, + except URLError: + return _post( + self._endpoint, + data, + self._request_headers, + timeout_sec, + self._ssl_context, ) - return resp def _export_with_retries( self, @@ -210,18 +198,17 @@ def _export_with_retries( backoff_seconds = 2**retry_num * uniform(0.8, 1.2) export_error: Exception | None = None try: - resp = self._export(serialized_data, deadline_sec - time()) - if resp.ok: + status_code, reason = self._export( + serialized_data, deadline_sec - time() + ) + if status_code < 400: return MetricExportResult.SUCCESS - except RequestException as error: - reason = error + retryable = _is_retryable(status_code) + except URLError as error: + reason = error.reason export_error = error - retryable = isinstance(error, ConnectionError) + retryable = True status_code = None - else: - reason = resp.reason - retryable = _is_retryable(resp) - status_code = resp.status_code if not retryable: _logger.error( @@ -299,7 +286,6 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return self._shutdown = True self._shutdown_in_progress.set() - self._session.close() def force_flush(self, timeout_millis: float = 10_000) -> bool: return True diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/__init__.py index ed8988a078b..65f4c61cd2f 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/src/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/__init__.py @@ -10,12 +10,10 @@ from random import uniform from threading import Event from time import time +from urllib.error import URLError from urllib.parse import urlparse from zlib import compress -from requests import Session -from requests.exceptions import ConnectionError, RequestException - from opentelemetry.exporter.otlp.pyproto.common._exporter_metrics import ( create_exporter_metrics, ) @@ -27,12 +25,12 @@ Compression, ) from opentelemetry.exporter.otlp.pyproto.http._common import ( + _build_ssl_context, _is_retryable, - _load_session_from_envvar, + _post, ) from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER, OTEL_EXPORTER_OTLP_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE, OTEL_EXPORTER_OTLP_CLIENT_KEY, @@ -77,7 +75,6 @@ def __init__( headers: dict[str, str] | None = None, timeout: float | None = None, compression: Compression | None = None, - session: Session | None = None, *, meter_provider: MeterProvider | None = None, ): @@ -117,20 +114,12 @@ def __init__( ) ) self._compression = compression or _compression_from_env() - self._session = ( - session - or _load_session_from_envvar( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_TRACES_CREDENTIAL_PROVIDER - ) - or Session() - ) - self._session.headers.update(self._headers) - self._session.headers.update(_OTLP_HTTP_HEADERS) - self._session.headers.update(self._headers) + self._request_headers = {**_OTLP_HTTP_HEADERS, **self._headers} if self._compression is not Compression.NoCompression: - self._session.headers.update( - {"Content-Encoding": self._compression.value} - ) + self._request_headers["Content-Encoding"] = self._compression.value + self._ssl_context = _build_ssl_context( + self._certificate_file, self._client_cert + ) self._shutdown = False self._metrics = create_exporter_metrics( @@ -156,22 +145,21 @@ def _export(self, serialized_data: bytes, timeout_sec: float | None = None): if timeout_sec is None: timeout_sec = self._timeout try: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, + return _post( + self._endpoint, + data, + self._request_headers, + timeout_sec, + self._ssl_context, ) - except ConnectionError: - resp = self._session.post( - url=self._endpoint, - data=data, - verify=self._certificate_file, - timeout=timeout_sec, - cert=self._client_cert, + except URLError: + return _post( + self._endpoint, + data, + self._request_headers, + timeout_sec, + self._ssl_context, ) - return resp def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: if self._shutdown: @@ -185,18 +173,17 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: backoff_seconds = 2**retry_num * uniform(0.8, 1.2) export_error: Exception | None = None try: - resp = self._export(serialized_data, deadline_sec - time()) - if resp.ok: + status_code, reason = self._export( + serialized_data, deadline_sec - time() + ) + if status_code < 400: return SpanExportResult.SUCCESS - except RequestException as error: - reason = error + retryable = _is_retryable(status_code) + except URLError as error: + reason = error.reason export_error = error - retryable = isinstance(error, ConnectionError) + retryable = True status_code = None - else: - reason = resp.reason - retryable = _is_retryable(resp) - status_code = resp.status_code if not retryable: _logger.error( @@ -246,7 +233,6 @@ def shutdown(self): return self._shutdown = True self._shutdown_in_progress.set() - self._session.close() def force_flush(self, timeout_millis: int = 30000) -> bool: return True diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_common/test___init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_common/test___init__.py index b0ae44d85b6..f3427156c23 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_common/test___init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_common/test___init__.py @@ -4,7 +4,6 @@ from unittest.mock import Mock, patch import pytest -from requests import Session from opentelemetry.exporter.otlp.pyproto.http._common import ( _is_retryable, @@ -15,31 +14,31 @@ # ── _is_retryable ───────────────────────────────────────────────────────────── def test_is_retryable_408(): - assert _is_retryable(Mock(status_code=408)) is True + assert _is_retryable(408) is True def test_is_retryable_500(): - assert _is_retryable(Mock(status_code=500)) is True + assert _is_retryable(500) is True def test_is_retryable_503(): - assert _is_retryable(Mock(status_code=503)) is True + assert _is_retryable(503) is True def test_is_retryable_599(): - assert _is_retryable(Mock(status_code=599)) is True + assert _is_retryable(599) is True def test_is_retryable_200(): - assert _is_retryable(Mock(status_code=200)) is False + assert _is_retryable(200) is False def test_is_retryable_404(): - assert _is_retryable(Mock(status_code=404)) is False + assert _is_retryable(404) is False def test_is_retryable_400(): - assert _is_retryable(Mock(status_code=400)) is False + assert _is_retryable(400) is False # ── _load_session_from_envvar ───────────────────────────────────────────────── @@ -60,8 +59,8 @@ def test_load_session_raises_on_unknown_provider(): _load_session_from_envvar(_CRED_ENVVAR) -def test_load_session_returns_session_from_provider(): - mock_session = Session() +def test_load_session_returns_value_from_provider(): + mock_session = Mock() mock_ep = Mock() mock_ep.load.return_value = lambda: mock_session @@ -72,16 +71,3 @@ def test_load_session_returns_session_from_provider(): ): result = _load_session_from_envvar(_CRED_ENVVAR) assert result is mock_session - - -def test_load_session_raises_if_provider_returns_non_session(): - mock_ep = Mock() - mock_ep.load.return_value = lambda: "not-a-session" - - with patch.dict("os.environ", {_GENERIC_ENVVAR: "bad_provider"}): - with patch( - "opentelemetry.exporter.otlp.pyproto.http._common.entry_points", - return_value=iter([mock_ep]), - ): - with pytest.raises(RuntimeError, match="must be of type"): - _load_session_from_envvar(_CRED_ENVVAR) diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/test___init__.py index c95d16c188f..d95cac90446 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/test___init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/_log_exporter/test___init__.py @@ -4,10 +4,8 @@ # pylint: disable=protected-access import unittest -from unittest.mock import Mock, patch - -from requests import Session -from requests.exceptions import ConnectionError +from unittest.mock import patch +from urllib.error import URLError from opentelemetry.exporter.otlp.pyproto.http import Compression from opentelemetry.exporter.otlp.pyproto.http._log_exporter import ( @@ -23,15 +21,15 @@ def _ok(): - return Mock(ok=True, status_code=200) + return (200, "OK") def _503(): - return Mock(ok=False, status_code=503, reason="Service Unavailable") + return (503, "Service Unavailable") def _404(): - return Mock(ok=False, status_code=404, reason="Not Found") + return (404, "Not Found") class TestOTLPLogExporter(unittest.TestCase): @@ -46,9 +44,8 @@ def test_constructor_defaults(self): ) self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertEqual(exporter._compression, Compression.NoCompression) - self.assertIsInstance(exporter._session, Session) self.assertEqual( - exporter._session.headers["Content-Type"], "application/x-protobuf" + exporter._request_headers["Content-Type"], "application/x-protobuf" ) self.assertFalse(exporter._shutdown) exporter.shutdown() @@ -101,14 +98,14 @@ def test_compression_env_var_gzip(self): with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip"}): exporter = OTLPLogExporter() self.assertEqual(exporter._compression, Compression.Gzip) - self.assertEqual(exporter._session.headers.get("Content-Encoding"), "gzip") + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") exporter.shutdown() def test_compression_env_var_deflate(self): with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "deflate"}): exporter = OTLPLogExporter() self.assertEqual(exporter._compression, Compression.Deflate) - self.assertEqual(exporter._session.headers.get("Content-Encoding"), "deflate") + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "deflate") exporter.shutdown() def test_compression_logs_env_overrides_generic(self): @@ -123,25 +120,19 @@ def test_compression_logs_env_overrides_generic(self): def test_compression_arg_gzip(self): exporter = OTLPLogExporter(compression=Compression.Gzip) self.assertEqual(exporter._compression, Compression.Gzip) - self.assertEqual(exporter._session.headers.get("Content-Encoding"), "gzip") + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") exporter.shutdown() def test_no_compression_header_for_none_compression(self): exporter = OTLPLogExporter(compression=Compression.NoCompression) - self.assertNotIn("Content-Encoding", exporter._session.headers) - exporter.shutdown() - - def test_custom_session_used(self): - custom_session = Session() - exporter = OTLPLogExporter(session=custom_session) - self.assertIs(exporter._session, custom_session) + self.assertNotIn("Content-Encoding", exporter._request_headers) exporter.shutdown() # ── export ──────────────────────────────────────────────────────────────── def test_export_success(self): exporter = OTLPLogExporter() - with patch.object(exporter._session, "post", return_value=_ok()): + with patch.object(exporter, "_export", return_value=_ok()): result = exporter.export([]) self.assertEqual(result, LogRecordExportResult.SUCCESS) exporter.shutdown() @@ -154,14 +145,14 @@ def test_export_after_shutdown_returns_failure(self): def test_export_non_retryable_failure(self): exporter = OTLPLogExporter() - with patch.object(exporter._session, "post", return_value=_404()): + with patch.object(exporter, "_export", return_value=_404()): result = exporter.export([]) self.assertEqual(result, LogRecordExportResult.FAILURE) exporter.shutdown() def test_export_retry_then_deadline_exceeded(self): exporter = OTLPLogExporter(timeout=0.01) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=100.0): result = exporter.export([]) self.assertEqual(result, LogRecordExportResult.FAILURE) @@ -169,7 +160,7 @@ def test_export_retry_then_deadline_exceeded(self): def test_export_max_retries_exhausted(self): exporter = OTLPLogExporter(timeout=100) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=0.0001): with patch.object(exporter._shutdown_is_occuring, "wait", return_value=False): result = exporter.export([]) @@ -178,7 +169,7 @@ def test_export_max_retries_exhausted(self): def test_shutdown_interrupts_retry(self): exporter = OTLPLogExporter(timeout=100) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=0.0001): with patch.object(exporter._shutdown_is_occuring, "wait", return_value=True): result = exporter.export([]) @@ -187,7 +178,7 @@ def test_shutdown_interrupts_retry(self): def test_connection_error_is_retryable(self): exporter = OTLPLogExporter(timeout=0.01) - with patch.object(exporter, "_export", side_effect=ConnectionError("refused")): + with patch.object(exporter, "_export", side_effect=URLError("refused")): with patch(_UNIFORM, return_value=100.0): result = exporter.export([]) self.assertEqual(result, LogRecordExportResult.FAILURE) diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/test___init__.py index bec3dcefb14..4930ddf9acc 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/test___init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/metric_exporter/test___init__.py @@ -4,10 +4,7 @@ # pylint: disable=protected-access import unittest -from unittest.mock import Mock, patch - -from requests import Session -from requests.exceptions import ConnectionError +from unittest.mock import patch from opentelemetry.exporter.otlp.pyproto.common._internal.metrics_encoder import ( encode_metrics, @@ -38,15 +35,15 @@ def _ok(): - return Mock(ok=True, status_code=200) + return (200, "OK") def _503(): - return Mock(ok=False, status_code=503, reason="Service Unavailable") + return (503, "Service Unavailable") def _404(): - return Mock(ok=False, status_code=404, reason="Not Found") + return (404, "Not Found") def _make_metrics_data(n_gauge_points: int = 1) -> MetricsData: @@ -100,9 +97,8 @@ def test_constructor_defaults(self): ) self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertEqual(exporter._compression, Compression.NoCompression) - self.assertIsInstance(exporter._session, Session) self.assertEqual( - exporter._session.headers["Content-Type"], "application/x-protobuf" + exporter._request_headers["Content-Type"], "application/x-protobuf" ) self.assertIsNone(exporter._max_export_batch_size) self.assertFalse(exporter._shutdown) @@ -150,7 +146,7 @@ def test_compression_env_var_gzip(self): with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip"}): exporter = OTLPMetricExporter() self.assertEqual(exporter._compression, Compression.Gzip) - self.assertEqual(exporter._session.headers.get("Content-Encoding"), "gzip") + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") exporter.shutdown() def test_compression_metrics_env_overrides_generic(self): @@ -167,24 +163,18 @@ def test_max_export_batch_size_arg(self): self.assertEqual(exporter._max_export_batch_size, 100) exporter.shutdown() - def test_custom_session_used(self): - custom_session = Session() - exporter = OTLPMetricExporter(session=custom_session) - self.assertIs(exporter._session, custom_session) - exporter.shutdown() - # ── export ──────────────────────────────────────────────────────────────── def test_export_success_empty_data(self): exporter = OTLPMetricExporter() - with patch.object(exporter._session, "post", return_value=_ok()): + with patch.object(exporter, "_export", return_value=_ok()): result = exporter.export(_empty_metrics_data()) self.assertEqual(result, MetricExportResult.SUCCESS) exporter.shutdown() def test_export_success_with_data(self): exporter = OTLPMetricExporter() - with patch.object(exporter._session, "post", return_value=_ok()): + with patch.object(exporter, "_export", return_value=_ok()): result = exporter.export(_make_metrics_data(2)) self.assertEqual(result, MetricExportResult.SUCCESS) exporter.shutdown() @@ -197,14 +187,14 @@ def test_export_after_shutdown_returns_failure(self): def test_export_non_retryable_failure(self): exporter = OTLPMetricExporter() - with patch.object(exporter._session, "post", return_value=_404()): + with patch.object(exporter, "_export", return_value=_404()): result = exporter.export(_empty_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) exporter.shutdown() def test_export_retry_then_deadline_exceeded(self): exporter = OTLPMetricExporter(timeout=0.01) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=100.0): result = exporter.export(_empty_metrics_data()) self.assertEqual(result, MetricExportResult.FAILURE) @@ -212,7 +202,7 @@ def test_export_retry_then_deadline_exceeded(self): def test_export_max_retries_exhausted(self): exporter = OTLPMetricExporter(timeout=100) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=0.0001): with patch.object(exporter._shutdown_in_progress, "wait", return_value=False): result = exporter.export(_empty_metrics_data()) @@ -221,7 +211,7 @@ def test_export_max_retries_exhausted(self): def test_shutdown_interrupts_retry(self): exporter = OTLPMetricExporter(timeout=100) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=0.0001): with patch.object(exporter._shutdown_in_progress, "wait", return_value=True): result = exporter.export(_empty_metrics_data()) @@ -233,12 +223,12 @@ def test_export_with_batch_size_sends_multiple_requests(self): exporter = OTLPMetricExporter(max_export_batch_size=1) call_count = 0 - def post_side_effect(**_kwargs): + def export_side_effect(*_args, **_kwargs): nonlocal call_count call_count += 1 return _ok() - with patch.object(exporter._session, "post", side_effect=post_side_effect): + with patch.object(exporter, "_export", side_effect=export_side_effect): result = exporter.export(_make_metrics_data(3)) self.assertEqual(result, MetricExportResult.SUCCESS) self.assertEqual(call_count, 3) diff --git a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/test___init__.py b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/test___init__.py index 1f12847bb17..dffe404a254 100644 --- a/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/test___init__.py +++ b/exporter/opentelemetry-exporter-otlp-pyproto-http/tests/opentelemetry/exporter/otlp/pyproto/http/trace_exporter/test___init__.py @@ -4,10 +4,8 @@ # pylint: disable=protected-access import unittest -from unittest.mock import Mock, patch - -from requests import Session -from requests.exceptions import ConnectionError +from unittest.mock import patch +from urllib.error import URLError from opentelemetry.exporter.otlp.pyproto.http import Compression from opentelemetry.exporter.otlp.pyproto.http.trace_exporter import ( @@ -23,15 +21,15 @@ def _ok(): - return Mock(ok=True, status_code=200) + return (200, "OK") def _503(): - return Mock(ok=False, status_code=503, reason="Service Unavailable") + return (503, "Service Unavailable") def _404(): - return Mock(ok=False, status_code=404, reason="Not Found") + return (404, "Not Found") class TestOTLPSpanExporter(unittest.TestCase): @@ -46,9 +44,8 @@ def test_constructor_defaults(self): ) self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT) self.assertEqual(exporter._compression, Compression.NoCompression) - self.assertIsInstance(exporter._session, Session) self.assertEqual( - exporter._session.headers["Content-Type"], "application/x-protobuf" + exporter._request_headers["Content-Type"], "application/x-protobuf" ) self.assertFalse(exporter._shutdown) exporter.shutdown() @@ -101,14 +98,14 @@ def test_compression_env_var_gzip(self): with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "gzip"}): exporter = OTLPSpanExporter() self.assertEqual(exporter._compression, Compression.Gzip) - self.assertEqual(exporter._session.headers.get("Content-Encoding"), "gzip") + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") exporter.shutdown() def test_compression_env_var_deflate(self): with patch.dict("os.environ", {"OTEL_EXPORTER_OTLP_COMPRESSION": "deflate"}): exporter = OTLPSpanExporter() self.assertEqual(exporter._compression, Compression.Deflate) - self.assertEqual(exporter._session.headers.get("Content-Encoding"), "deflate") + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "deflate") exporter.shutdown() def test_compression_traces_env_overrides_generic(self): @@ -123,25 +120,19 @@ def test_compression_traces_env_overrides_generic(self): def test_compression_arg_gzip(self): exporter = OTLPSpanExporter(compression=Compression.Gzip) self.assertEqual(exporter._compression, Compression.Gzip) - self.assertEqual(exporter._session.headers.get("Content-Encoding"), "gzip") + self.assertEqual(exporter._request_headers.get("Content-Encoding"), "gzip") exporter.shutdown() def test_no_compression_header_for_none_compression(self): exporter = OTLPSpanExporter(compression=Compression.NoCompression) - self.assertNotIn("Content-Encoding", exporter._session.headers) - exporter.shutdown() - - def test_custom_session_used(self): - custom_session = Session() - exporter = OTLPSpanExporter(session=custom_session) - self.assertIs(exporter._session, custom_session) + self.assertNotIn("Content-Encoding", exporter._request_headers) exporter.shutdown() # ── export ──────────────────────────────────────────────────────────────── def test_export_success(self): exporter = OTLPSpanExporter() - with patch.object(exporter._session, "post", return_value=_ok()): + with patch.object(exporter, "_export", return_value=_ok()): result = exporter.export([]) self.assertEqual(result, SpanExportResult.SUCCESS) exporter.shutdown() @@ -154,7 +145,7 @@ def test_export_after_shutdown_returns_failure(self): def test_export_non_retryable_failure(self): exporter = OTLPSpanExporter() - with patch.object(exporter._session, "post", return_value=_404()): + with patch.object(exporter, "_export", return_value=_404()): result = exporter.export([]) self.assertEqual(result, SpanExportResult.FAILURE) exporter.shutdown() @@ -162,7 +153,7 @@ def test_export_non_retryable_failure(self): def test_export_retry_then_deadline_exceeded(self): # backoff(100) > remaining timeout(0.01) → exits on first retry exporter = OTLPSpanExporter(timeout=0.01) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=100.0): result = exporter.export([]) self.assertEqual(result, SpanExportResult.FAILURE) @@ -170,7 +161,7 @@ def test_export_retry_then_deadline_exceeded(self): def test_export_max_retries_exhausted(self): exporter = OTLPSpanExporter(timeout=100) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=0.0001): with patch.object(exporter._shutdown_in_progress, "wait", return_value=False): result = exporter.export([]) @@ -179,7 +170,7 @@ def test_export_max_retries_exhausted(self): def test_shutdown_interrupts_retry(self): exporter = OTLPSpanExporter(timeout=100) - with patch.object(exporter._session, "post", return_value=_503()): + with patch.object(exporter, "_export", return_value=_503()): with patch(_UNIFORM, return_value=0.0001): with patch.object(exporter._shutdown_in_progress, "wait", return_value=True): result = exporter.export([]) @@ -187,9 +178,9 @@ def test_shutdown_interrupts_retry(self): exporter.shutdown() def test_connection_error_is_retryable(self): - # ConnectionError from _export goes into the retry path; deadline kills it fast. + # URLError from _export goes into the retry path; deadline kills it fast. exporter = OTLPSpanExporter(timeout=0.01) - with patch.object(exporter, "_export", side_effect=ConnectionError("refused")): + with patch.object(exporter, "_export", side_effect=URLError("refused")): with patch(_UNIFORM, return_value=100.0): result = exporter.export([]) self.assertEqual(result, SpanExportResult.FAILURE) diff --git a/uv.lock b/uv.lock index ddcf9c6d4e7..4507ec4a318 100644 --- a/uv.lock +++ b/uv.lock @@ -1082,7 +1082,6 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-pyproto-common" }, { name = "opentelemetry-sdk" }, - { name = "requests" }, ] [package.dev-dependencies] @@ -1100,7 +1099,6 @@ requires-dist = [ { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-pyproto-common", editable = "exporter/opentelemetry-exporter-otlp-pyproto-common" }, { name = "opentelemetry-sdk", editable = "opentelemetry-sdk" }, - { name = "requests", specifier = "~=2.7" }, ] [package.metadata.requires-dev]