Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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/5215.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: add `_resolve_component` shared utility for declarative config plugin loading, reducing boilerplate in exporter factory functions
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,45 @@ def load_entry_point(group: str, name: str) -> type:
) from exc


def _resolve_component(
config,
Comment thread
MikeGoldsmith marked this conversation as resolved.
Outdated
registry: dict,
Comment thread
MikeGoldsmith marked this conversation as resolved.
Outdated
entry_point_group: str,
component_type: str,
):
"""Resolve a config dataclass to a component instance.

Checks built-in factories in ``registry`` first (by matching typed
field names on ``config``), then falls back to entry point loading
for plugin components found in ``config.additional_properties``.

Args:
config: A dataclass with ``additional_properties`` (from the
``@_additional_properties`` decorator).
registry: Mapping of built-in component names to factory
callables. Each factory receives the field value from config.
entry_point_group: The entry point group name for plugin loading.
component_type: Human-readable name for error messages
(e.g. "span exporter").

Returns:
The resolved component instance.

Raises:
ConfigurationError: If no component type is specified in config.
"""
for name, factory in registry.items():
value = getattr(config, name, None)
if value is not None:
return factory(value)
if config.additional_properties:
name, plugin_config = next(iter(config.additional_properties.items()))
Comment thread
aabmass marked this conversation as resolved.
return load_entry_point(entry_point_group, name)(
**(plugin_config or {})
)
raise ConfigurationError(f"No {component_type} type specified in config.")


def _parse_headers(
headers: list | None,
headers_list: str | None,
Expand Down
72 changes: 72 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
_additional_properties,
_map_compression,
_parse_headers,
_resolve_component,
load_entry_point,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
Expand Down Expand Up @@ -289,3 +290,74 @@ def test_log_record_exporter(self):

def test_push_metric_exporter(self):
self._assert_supports_additional_properties(PushMetricExporter)


class TestResolveComponent(unittest.TestCase):
def setUp(self):
@_additional_properties
@dataclass
class _Config:
builtin_a: dict | None = None
builtin_b: str | None = None
additional_properties: ClassVar[dict[str, Any]]

self.cls = _Config
self.registry = {
"builtin_a": lambda v: ("resolved_a", v),
"builtin_b": lambda v: ("resolved_b", v),
}

def test_resolves_builtin_from_registry(self):
config = self.cls(builtin_a={"key": "val"})
result = _resolve_component(
config, self.registry, "test_group", "test component"
)
self.assertEqual(result, ("resolved_a", {"key": "val"}))

def test_resolves_plugin_via_entry_point(self):
mock_instance = MagicMock()
mock_class = MagicMock(return_value=mock_instance)
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[MagicMock(**{"load.return_value": mock_class})],
):
# pylint: disable=unexpected-keyword-arg
config = self.cls(my_plugin={"opt": "val"})
result = _resolve_component(
config, self.registry, "test_group", "test component"
)
self.assertIs(result, mock_instance)
mock_class.assert_called_once_with(opt="val")

Comment thread
MikeGoldsmith marked this conversation as resolved.
def test_plugin_with_empty_config(self):
mock_instance = MagicMock()
mock_class = MagicMock(return_value=mock_instance)
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[MagicMock(**{"load.return_value": mock_class})],
):
# pylint: disable=unexpected-keyword-arg
config = self.cls(my_plugin={})
_resolve_component(
config, self.registry, "test_group", "test component"
)
mock_class.assert_called_once_with()

def test_no_component_raises_configuration_error(self):
config = self.cls()
with self.assertRaises(ConfigurationError):
_resolve_component(
config, self.registry, "test_group", "test component"
)

def test_plugin_not_found_raises_configuration_error(self):
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
):
# pylint: disable=unexpected-keyword-arg
config = self.cls(missing_plugin={})
with self.assertRaises(ConfigurationError):
_resolve_component(
config, self.registry, "test_group", "test component"
)
Loading