Skip to content
Open
Show file tree
Hide file tree
Changes from all 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",
]

[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]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to register these all the time? Won't that cause them to always use their entrypoints?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically these aren't needed, but I've added them for consistency (easy to search for all transport implementations) and to provide an example for users to provide their own entry-points (we follow the same pattern in the SDK). We do the check for known entry-points before we attempt to load anything, so these are still optimized away.

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,
)

return RequestsHTTPTransport


def _load_urllib3_transport() -> type[BaseHTTPTransport]:
# 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,
)

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,
cert: str | tuple[str, str] | None,
**kwargs: Any,
) -> Self:
Comment thread
herin049 marked this conversation as resolved.
"""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
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: dict[str, 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: dict[str, 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


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: dict[str, Any],
) -> Self:
return cls(verify=verify, cert=cert, **kwargs)

def __init__(
self,
*,
verify: bool | str = True,
cert: str | tuple[str, str] | None = None,
**kwargs: dict[str, 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