diff --git a/.changelog/5320.added b/.changelog/5320.added new file mode 100644 index 0000000000..ab71930829 --- /dev/null +++ b/.changelog/5320.added @@ -0,0 +1 @@ +`opentelemetry-exporter-http-transport`: enable entry-point loading of transport implementations diff --git a/exporter/opentelemetry-exporter-http-transport/pyproject.toml b/exporter/opentelemetry-exporter-http-transport/pyproject.toml index e2ecb426cb..40ca4774a5 100644 --- a/exporter/opentelemetry-exporter-http-transport/pyproject.toml +++ b/exporter/opentelemetry-exporter-http-transport/pyproject.toml @@ -25,7 +25,9 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] -dependencies = [] +dependencies = [ + "opentelemetry-api ~= 1.15", +] [project.optional-dependencies] urllib3 = [ @@ -39,6 +41,10 @@ requests = [ Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-http-transport" Repository = "https://github.com/open-telemetry/opentelemetry-python" +[project.entry-points.opentelemetry_http_transport] +urllib3 = "opentelemetry.exporter.http.transport._urllib3:Urllib3HTTPTransport" +requests = "opentelemetry.exporter.http.transport._requests:RequestsHTTPTransport" + [tool.hatch.version] path = "src/opentelemetry/exporter/http/transport/version/__init__.py" diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py index e57cf4aba9..c8cfcc4ca2 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/__init__.py @@ -1,2 +1,76 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +# pylint: disable-next=import-error +from opentelemetry.exporter.http.transport._requests import ( + RequestsHTTPTransport as _RequestsHTTPTransport, +) + +# pylint: disable-next=import-error +from opentelemetry.exporter.http.transport._urllib3 import ( + Urllib3HTTPTransport as _Urllib3HTTPTransport, +) + +if TYPE_CHECKING: + from typing import Any, Protocol + + from opentelemetry.exporter.http.transport._base import BaseHTTPTransport + + class BaseHTTPTransportFactory(Protocol): + def __call__( + self, + *, + verify: bool | str, + cert: str | tuple[str, str] | None, + **kwargs: Any, + ) -> BaseHTTPTransport: ... + + +_KNOWN_TRANSPORTS: dict[str, BaseHTTPTransportFactory] = { + "requests": _RequestsHTTPTransport, + "urllib3": _Urllib3HTTPTransport, +} + + +def _load_http_transport_factory(name: str) -> BaseHTTPTransportFactory: + """Return the transport factory registered under *name*. + + Checks the built-in transport registry first to avoid the overhead of an + entry-point scan for built-in transports. Falls back to standard entry-point + discovery for user supplied transports registered under the + ``opentelemetry_http_transport`` group. + + :param name: Entry point name, e.g. ``"requests"`` or ``"urllib3"``. + :returns: A callable with signature + ``(*, verify, cert, **kwargs) -> BaseHTTPTransport``. + :raises ValueError: If no transport is registered under *name*. + :raises TypeError: If the loaded entry point is not callable. + """ + if name in _KNOWN_TRANSPORTS: + return _KNOWN_TRANSPORTS[name] + # pylint: disable-next=import-outside-toplevel,import-error + from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 + entry_points, + ) + + ep = next( + iter(entry_points(group="opentelemetry_http_transport", name=name)), + None, + ) + if not ep: + raise ValueError( + f"No HTTP transport registered under name {name!r}. " + "Install the corresponding extra or register an entry point " + "under the 'opentelemetry_http_transport' group." + ) + factory = ep.load() + if not callable(factory): + raise TypeError( + f"Transport {name!r} loaded from entry point is not callable " + f"(got {factory!r})." + ) + return cast("BaseHTTPTransportFactory", factory) diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py index 20602ac3d4..685b3e5b92 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_base.py @@ -1,11 +1,16 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import json from abc import ABC, abstractmethod -from collections.abc import Mapping from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any @dataclass(frozen=True, slots=True) diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py index 637fd93b51..9ad205e3f8 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_requests.py @@ -4,9 +4,8 @@ from __future__ import annotations import functools -from collections.abc import Mapping from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._base import ( @@ -15,6 +14,9 @@ ) if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any + import requests from requests import Response @@ -66,6 +68,7 @@ def __init__( verify: bool | str = True, cert: str | tuple[str, str] | None = None, session: requests.Session | None = None, + **kwargs: Any, ) -> None: # pylint: disable-next=import-outside-toplevel import requests # noqa: PLC0415 diff --git a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py index 57a68bd967..2f2fd72954 100644 --- a/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py +++ b/exporter/opentelemetry-exporter-http-transport/src/opentelemetry/exporter/http/transport/_urllib3.py @@ -5,9 +5,8 @@ import functools import json -from collections.abc import Mapping from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING # pylint: disable-next=import-error from opentelemetry.exporter.http.transport._base import ( @@ -16,6 +15,9 @@ ) if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any + from urllib3 import BaseHTTPResponse @@ -70,6 +72,7 @@ def __init__( *, verify: bool | str = True, cert: str | tuple[str, str] | None = None, + **kwargs: Any, ) -> None: # pylint: disable-next=import-outside-toplevel import urllib3 # noqa: PLC0415 diff --git a/exporter/opentelemetry-exporter-http-transport/test-requirements.in b/exporter/opentelemetry-exporter-http-transport/test-requirements.in index e8b1ac224d..c9de583437 100644 --- a/exporter/opentelemetry-exporter-http-transport/test-requirements.in +++ b/exporter/opentelemetry-exporter-http-transport/test-requirements.in @@ -3,4 +3,5 @@ mocket==3.14.1 packaging==26.2 pluggy==1.6.0 pytest==7.4.4 +-e opentelemetry-api -e exporter/opentelemetry-exporter-http-transport[urllib3,requests] diff --git a/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt b/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt index b1ec00aea6..c2d364fb3d 100644 --- a/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt +++ b/exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt @@ -2,6 +2,10 @@ # uv pip compile --python 3.10 --universal --resolution highest exporter/opentelemetry-exporter-http-transport/test-requirements.in -o exporter/opentelemetry-exporter-http-transport/test-requirements.latest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in +-e opentelemetry-api + # via + # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in + # opentelemetry-exporter-http-transport certifi==2026.4.22 # via requests charset-normalizer==3.4.7 @@ -44,6 +48,7 @@ typing-extensions==4.15.0 # via # exceptiongroup # mocket + # opentelemetry-api urllib3==2.7.0 # via # mocket diff --git a/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt b/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt index c9b33dc117..63a0f998c8 100644 --- a/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt +++ b/exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt @@ -2,6 +2,10 @@ # uv pip compile --python 3.10 --universal --resolution lowest-direct exporter/opentelemetry-exporter-http-transport/test-requirements.in -o exporter/opentelemetry-exporter-http-transport/test-requirements.oldest.txt -e exporter/opentelemetry-exporter-http-transport # via -r exporter/opentelemetry-exporter-http-transport/test-requirements.in +-e opentelemetry-api + # via + # -r exporter/opentelemetry-exporter-http-transport/test-requirements.in + # opentelemetry-exporter-http-transport certifi==2026.4.22 # via requests chardet==3.0.4 @@ -44,6 +48,7 @@ typing-extensions==4.15.0 # via # exceptiongroup # mocket + # opentelemetry-api urllib3==1.26.20 # via # mocket diff --git a/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py new file mode 100644 index 0000000000..dcc253c203 --- /dev/null +++ b/exporter/opentelemetry-exporter-http-transport/tests/test_load_transport.py @@ -0,0 +1,61 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# pylint: disable=import-error + +import unittest +from unittest.mock import MagicMock, patch + +from opentelemetry.exporter.http.transport import _load_http_transport_factory +from opentelemetry.exporter.http.transport._requests import ( + RequestsHTTPTransport, +) +from opentelemetry.exporter.http.transport._urllib3 import ( + Urllib3HTTPTransport, +) + +_ENTRY_POINTS_TARGET = "opentelemetry.util._importlib_metadata.entry_points" + + +# pylint: disable=no-self-use +class TestLoadHTTPTransportFactory(unittest.TestCase): + def test_returns_requests_transport(self): + self.assertIs( + _load_http_transport_factory("requests"), RequestsHTTPTransport + ) + + def test_returns_urllib3_transport(self): + self.assertIs( + _load_http_transport_factory("urllib3"), Urllib3HTTPTransport + ) + + def test_known_transport_does_not_call_entry_points(self): + with patch(_ENTRY_POINTS_TARGET) as mock_ep: + _load_http_transport_factory("requests") + _load_http_transport_factory("urllib3") + self.assertFalse(mock_ep.called) + + def test_unknown_transport_calls_entry_points(self): + def _custom_factory(*, verify, cert, **kwargs): + pass + + mock_ep = MagicMock() + mock_ep.load.return_value = _custom_factory + with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]) as mock_fn: + result = _load_http_transport_factory("custom") + self.assertEqual( + mock_fn.call_args.kwargs, + {"group": "opentelemetry_http_transport", "name": "custom"}, + ) + self.assertIs(result, _custom_factory) + + def test_entry_point_non_callable_raises_type_error(self): + mock_ep = MagicMock() + mock_ep.load.return_value = "not_callable" + with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]): + self.assertRaises(TypeError, _load_http_transport_factory, "bad") + + def test_unknown_transport_raises_value_error(self): + with patch(_ENTRY_POINTS_TARGET, return_value=[]): + self.assertRaises( + ValueError, _load_http_transport_factory, "nonexistent" + ) diff --git a/uv.lock b/uv.lock index e307a06639..ec806543d8 100644 --- a/uv.lock +++ b/uv.lock @@ -845,6 +845,9 @@ wheels = [ [[package]] name = "opentelemetry-exporter-http-transport" source = { editable = "exporter/opentelemetry-exporter-http-transport" } +dependencies = [ + { name = "opentelemetry-api" }, +] [package.optional-dependencies] requests = [ @@ -856,6 +859,7 @@ urllib3 = [ [package.metadata] requires-dist = [ + { name = "opentelemetry-api", editable = "opentelemetry-api" }, { name = "requests", marker = "extra == 'requests'", specifier = "~=2.25" }, { name = "urllib3", marker = "extra == 'urllib3'", specifier = ">=1.26" }, ]