Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5320.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-exporter-http-transport`: enable entry-point loading of transport implementations
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = []
dependencies = [
"opentelemetry-api ~= 1.15",
]
Comment thread
herin049 marked this conversation as resolved.

[project.optional-dependencies]
urllib3 = [
Expand All @@ -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]
Comment thread
MikeGoldsmith marked this conversation as resolved.
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"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,79 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Callable

from opentelemetry.exporter.http.transport._base import BaseHTTPTransport


def _load_requests_transport() -> type[BaseHTTPTransport]:
# pylint: disable-next=import-outside-toplevel,import-error
from opentelemetry.exporter.http.transport._requests import ( # noqa: PLC0415
RequestsHTTPTransport,
)
Comment thread
herin049 marked this conversation as resolved.
Outdated

return RequestsHTTPTransport


def _load_urllib3_transport() -> type[BaseHTTPTransport]:
Comment thread
herin049 marked this conversation as resolved.
Outdated
# pylint: disable-next=import-outside-toplevel,import-error
from opentelemetry.exporter.http.transport._urllib3 import ( # noqa: PLC0415
Urllib3HTTPTransport,
)

return Urllib3HTTPTransport


_KNOWN_TRANSPORTS: dict[str, Callable[[], type[BaseHTTPTransport]]] = {
"requests": _load_requests_transport,
"urllib3": _load_urllib3_transport,
}


def _load_http_transport_class(name: str) -> type[BaseHTTPTransport]:
"""Return the transport class 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: The :class:`~opentelemetry.exporter.http.transport._base.BaseHTTPTransport`
subclass registered under *name*.
:raises ValueError: If no transport is registered under *name*.
"""
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."
)
cls = ep.load()
# pylint: disable-next=import-outside-toplevel,import-error
from opentelemetry.exporter.http.transport._base import ( # noqa: PLC0415
BaseHTTPTransport,
)
Comment thread
herin049 marked this conversation as resolved.
Outdated

if not isinstance(cls, type) or not issubclass(cls, BaseHTTPTransport):
raise TypeError(
f"Transport {name!r} loaded from entry point does not subclass "
f"BaseHTTPTransport (got {cls!r})."
)
return cls
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
# 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

from typing_extensions import Self


@dataclass(frozen=True, slots=True)
Expand Down Expand Up @@ -57,6 +64,30 @@ def json(self) -> Any:
class BaseHTTPTransport(ABC):
"""Abstract HTTP transport interface used by HTTP exporters."""

@classmethod
@abstractmethod
def create(
cls,
verify: bool | str,
Comment thread
herin049 marked this conversation as resolved.
Outdated
cert: str | tuple[str, str] | None,
**kwargs: Any,
) -> Self:
Comment thread
herin049 marked this conversation as resolved.
Outdated
"""Create a new transport instance.

:param verify: Controls TLS certificate verification. ``True`` verifies
the server certificate against the system CA bundle, ``False``
disables verification, a string is interpreted as a path to a CA
bundle file to use instead of the system bundle.
:param cert: Client certificate for mutual TLS. Pass a string path to a
PEM-encoded certificate file (the file must contain both the
certificate and the key), or a ``(cert_file, key_file)`` tuple when
the private key is in a separate file. Pass ``None`` to disable
client side authentication.
:param kwargs: Additional keyword arguments forwarded to the transport
implementation.
:returns: A new transport instance.
"""

@abstractmethod
def request(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -15,8 +14,12 @@
)

if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Any

import requests
from requests import Response
Comment thread
herin049 marked this conversation as resolved.
from typing_extensions import Self


@functools.cache
Expand Down Expand Up @@ -60,12 +63,22 @@ def headers(self) -> Mapping[str, str]:


class RequestsHTTPTransport(BaseHTTPTransport):
@classmethod
def create(
cls,
verify: bool | str,
cert: str | tuple[str, str] | None,
**kwargs: Any,
) -> Self:
return cls(verify=verify, cert=cert, **kwargs)

def __init__(
self,
*,
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -16,6 +15,10 @@
)

if TYPE_CHECKING:
from collections.abc import Mapping
from typing import Any

from typing_extensions import Self
from urllib3 import BaseHTTPResponse

Comment thread
herin049 marked this conversation as resolved.

Expand Down Expand Up @@ -65,11 +68,21 @@ def json(self) -> Any:


class Urllib3HTTPTransport(BaseHTTPTransport):
@classmethod
def create(
cls,
verify: bool | str,
cert: str | tuple[str, str] | None,
**kwargs: Any,
Comment thread
herin049 marked this conversation as resolved.
Outdated
) -> Self:
return cls(verify=verify, cert=cert, **kwargs)

def __init__(
self,
*,
verify: bool | str = True,
cert: str | tuple[str, str] | None = None,
**kwargs: Any,
) -> None:
# pylint: disable-next=import-outside-toplevel
import urllib3 # noqa: PLC0415
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,6 +48,7 @@ typing-extensions==4.15.0
# via
# exceptiongroup
# mocket
# opentelemetry-api
urllib3==2.7.0
# via
# mocket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,6 +48,7 @@ typing-extensions==4.15.0
# via
# exceptiongroup
# mocket
# opentelemetry-api
urllib3==1.26.20
# via
# mocket
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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_class
from opentelemetry.exporter.http.transport._base import BaseHTTPTransport
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 TestLoadHTTPTransportClass(unittest.TestCase):
def test_returns_requests_transport(self):
self.assertIs(
_load_http_transport_class("requests"), RequestsHTTPTransport
)

def test_returns_urllib3_transport(self):
self.assertIs(
_load_http_transport_class("urllib3"), Urllib3HTTPTransport
)

def test_known_transport_does_not_call_entry_points(self):
with patch(_ENTRY_POINTS_TARGET) as mock_ep:
_load_http_transport_class("requests")
_load_http_transport_class("urllib3")
self.assertFalse(mock_ep.called)

def test_unknown_transport_calls_entry_points(self):
class _CustomTransport:
pass

BaseHTTPTransport.register(_CustomTransport)
mock_ep = MagicMock()
mock_ep.load.return_value = _CustomTransport
with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]) as mock_fn:
result = _load_http_transport_class("custom")
self.assertEqual(
mock_fn.call_args.kwargs,
{"group": "opentelemetry_http_transport", "name": "custom"},
)
self.assertIs(result, _CustomTransport)

def test_entry_point_non_subclass_raises_type_error(self):
class _NotATransport:
pass

mock_ep = MagicMock()
mock_ep.load.return_value = _NotATransport
with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]):
self.assertRaises(TypeError, _load_http_transport_class, "bad")

def test_entry_point_non_class_raises_type_error(self):
mock_ep = MagicMock()
mock_ep.load.return_value = object()
with patch(_ENTRY_POINTS_TARGET, return_value=[mock_ep]):
self.assertRaises(TypeError, _load_http_transport_class, "bad")

def test_unknown_transport_raises_value_error(self):
with patch(_ENTRY_POINTS_TARGET, return_value=[]):
self.assertRaises(
ValueError, _load_http_transport_class, "nonexistent"
)
Loading
Loading