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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- `opentelemetry-sdk`: add `load_entry_point` shared utility to declarative file configuration for loading plugins via entry points; refactor propagator loading to use it
([#5093](https://github.com/open-telemetry/opentelemetry-python/pull/5093))
- `opentelemetry-sdk`: Add `create_logger_provider`/`configure_logger_provider` to declarative file configuration, enabling LoggerProvider instantiation from config files without reading env vars
([#4990](https://github.com/open-telemetry/opentelemetry-python/pull/4990))
- `opentelemetry-sdk`: Add `service` resource detector support to declarative file configuration via `detection_development.detectors[].service`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,39 @@
from __future__ import annotations

import logging
from typing import Optional
from typing import Optional, Type

from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.util._importlib_metadata import entry_points

_logger = logging.getLogger(__name__)


def load_entry_point(group: str, name: str) -> Type:
"""Load a plugin class from an entry point group by name.

Returns the loaded class — callers are responsible for instantiation
with whatever arguments their config requires.

Raises:
ConfigurationError: If the entry point is not found or fails to load.
"""
try:
ep = next(iter(entry_points(group=group, name=name)), None)
if ep is None:
raise ConfigurationError(
f"Plugin '{name}' not found in group '{group}'. "
"Make sure the package providing this plugin is installed."
)
return ep.load()
except ConfigurationError:
raise
except Exception as exc:
raise ConfigurationError(
f"Failed to load plugin '{name}' from group '{group}': {exc}"
) from exc


def _parse_headers(
headers: Optional[list],
headers_list: Optional[str],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.propagators.textmap import TextMapPropagator
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration._common import load_entry_point
from opentelemetry.sdk._configuration.models import (
Propagator as PropagatorConfig,
)
Expand All @@ -30,28 +30,11 @@
from opentelemetry.trace.propagation.tracecontext import (
TraceContextTextMapPropagator,
)
from opentelemetry.util._importlib_metadata import entry_points


def _load_entry_point_propagator(name: str) -> TextMapPropagator:
"""Load a propagator by name from the opentelemetry_propagator entry point group."""
try:
ep = next(
iter(entry_points(group="opentelemetry_propagator", name=name)),
None,
)
if not ep:
raise ConfigurationError(
f"Propagator '{name}' not found. "
"It may not be installed or may be misspelled."
)
return ep.load()()
except ConfigurationError:
raise
except Exception as exc:
raise ConfigurationError(
f"Failed to load propagator '{name}': {exc}"
) from exc
"""Load and instantiate a propagator by name."""
return load_entry_point("opentelemetry_propagator", name)()


def _propagators_from_textmap_config(
Expand Down
41 changes: 40 additions & 1 deletion opentelemetry-sdk/tests/_configuration/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@

import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

from opentelemetry.sdk._configuration._common import _parse_headers
from opentelemetry.sdk._configuration._common import (
_parse_headers,
load_entry_point,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError


class TestParseHeaders(unittest.TestCase):
Expand Down Expand Up @@ -79,3 +84,37 @@ def test_struct_headers_override_headers_list(self):

def test_both_empty_struct_and_none_list_returns_empty_dict(self):
self.assertEqual(_parse_headers([], None), {})


class TestLoadEntryPoint(unittest.TestCase):
def test_returns_loaded_class(self):
mock_class = MagicMock()
mock_ep = MagicMock()
mock_ep.load.return_value = mock_class
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
result = load_entry_point("some_group", "some_name")
self.assertIs(result, mock_class)

def test_raises_when_not_found(self):
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
):
with self.assertRaises(ConfigurationError) as ctx:
load_entry_point("some_group", "missing")
self.assertIn("missing", str(ctx.exception))
self.assertIn("some_group", str(ctx.exception))

def test_wraps_load_exception_in_configuration_error(self):
mock_ep = MagicMock()
mock_ep.load.side_effect = ImportError("bad import")
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
with self.assertRaises(ConfigurationError) as ctx:
load_entry_point("some_group", "some_name")
self.assertIn("bad import", str(ctx.exception))
18 changes: 9 additions & 9 deletions opentelemetry-sdk/tests/_configuration/test_propagator.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def test_b3_via_entry_point(self):
mock_ep.load.return_value = lambda: mock_propagator

with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
config = PropagatorConfig(
Expand All @@ -106,7 +106,7 @@ def test_b3multi_via_entry_point(self):
mock_ep.load.return_value = lambda: mock_propagator

with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
config = PropagatorConfig(
Expand All @@ -118,7 +118,7 @@ def test_b3multi_via_entry_point(self):

def test_b3_not_installed_raises_configuration_error(self):
with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
):
config = PropagatorConfig(
Expand All @@ -135,7 +135,7 @@ def test_composite_list_tracecontext(self):
mock_ep.load.return_value = lambda: mock_tc

with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
result = create_propagator(config)
Expand All @@ -158,7 +158,7 @@ def fake_entry_points(group, name):
return []

with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
side_effect=fake_entry_points,
):
config = PropagatorConfig(composite_list="tracecontext,baggage")
Expand All @@ -182,7 +182,7 @@ def test_composite_list_whitespace_around_names(self):
mock_ep.load.return_value = lambda: mock_tc

with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
config = PropagatorConfig(composite_list=" tracecontext ")
Expand All @@ -195,7 +195,7 @@ def test_entry_point_load_exception_raises_configuration_error(self):
mock_ep.load.side_effect = RuntimeError("package broken")

with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
config = PropagatorConfig(composite_list="broken-prop")
Expand All @@ -210,7 +210,7 @@ def test_deduplication_across_composite_and_composite_list(self):
mock_ep.load.return_value = lambda: mock_tc

with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
config = PropagatorConfig(
Expand All @@ -229,7 +229,7 @@ def test_deduplication_across_composite_and_composite_list(self):

def test_unknown_composite_list_propagator_raises(self):
with patch(
"opentelemetry.sdk._configuration._propagator.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
):
config = PropagatorConfig(composite_list="nonexistent")
Expand Down
Loading