Skip to content

Commit b6ffddd

Browse files
feat(config): add shared load_entry_point utility for declarative config plugin loading (#5093)
* add shared load_entry_point util for declarative config plugin loading Extracts a generic `load_entry_point(group, name)` helper into `_common` so that resource detector, exporter, propagator, and sampler plugin loading in declarative file config can all use the same entry point lookup pattern rather than duplicating it. Refactors `_propagator.py` to use the new util, removing its inline entry point lookup. Assisted-by: Claude Sonnet 4.6 * update CHANGELOG with PR number #5093 Assisted-by: Claude Sonnet 4.6 * add test clarifying instantiation errors are not wrapped load_entry_point returns the class; calling it is the caller's responsibility. This test documents that errors from instantiation propagate as-is rather than being wrapped in ConfigurationError. Assisted-by: Claude Opus 4.6
1 parent d2288d3 commit b6ffddd

File tree

5 files changed

+102
-31
lines changed

5 files changed

+102
-31
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## Unreleased
1414

15+
- `opentelemetry-sdk`: add `load_entry_point` shared utility to declarative file configuration for loading plugins via entry points; refactor propagator loading to use it
16+
([#5093](https://github.com/open-telemetry/opentelemetry-python/pull/5093))
1517
- `opentelemetry-sdk`: fix YAML structure injection via environment variable substitution in declarative file configuration; values containing newlines are now emitted as quoted YAML scalars per spec requirement
1618
([#5091](https://github.com/open-telemetry/opentelemetry-python/pull/5091))
1719
- `opentelemetry-sdk`: Add `create_logger_provider`/`configure_logger_provider` to declarative file configuration, enabling LoggerProvider instantiation from config files without reading env vars

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,39 @@
1515
from __future__ import annotations
1616

1717
import logging
18-
from typing import Optional
18+
from typing import Optional, Type
19+
20+
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
21+
from opentelemetry.util._importlib_metadata import entry_points
1922

2023
_logger = logging.getLogger(__name__)
2124

2225

26+
def load_entry_point(group: str, name: str) -> Type:
27+
"""Load a plugin class from an entry point group by name.
28+
29+
Returns the loaded class — callers are responsible for instantiation
30+
with whatever arguments their config requires.
31+
32+
Raises:
33+
ConfigurationError: If the entry point is not found or fails to load.
34+
"""
35+
try:
36+
ep = next(iter(entry_points(group=group, name=name)), None)
37+
if ep is None:
38+
raise ConfigurationError(
39+
f"Plugin '{name}' not found in group '{group}'. "
40+
"Make sure the package providing this plugin is installed."
41+
)
42+
return ep.load()
43+
except ConfigurationError:
44+
raise
45+
except Exception as exc:
46+
raise ConfigurationError(
47+
f"Failed to load plugin '{name}' from group '{group}': {exc}"
48+
) from exc
49+
50+
2351
def _parse_headers(
2452
headers: Optional[list],
2553
headers_list: Optional[str],

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from opentelemetry.propagate import set_global_textmap
2121
from opentelemetry.propagators.composite import CompositePropagator
2222
from opentelemetry.propagators.textmap import TextMapPropagator
23-
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
23+
from opentelemetry.sdk._configuration._common import load_entry_point
2424
from opentelemetry.sdk._configuration.models import (
2525
Propagator as PropagatorConfig,
2626
)
@@ -30,28 +30,11 @@
3030
from opentelemetry.trace.propagation.tracecontext import (
3131
TraceContextTextMapPropagator,
3232
)
33-
from opentelemetry.util._importlib_metadata import entry_points
3433

3534

3635
def _load_entry_point_propagator(name: str) -> TextMapPropagator:
37-
"""Load a propagator by name from the opentelemetry_propagator entry point group."""
38-
try:
39-
ep = next(
40-
iter(entry_points(group="opentelemetry_propagator", name=name)),
41-
None,
42-
)
43-
if not ep:
44-
raise ConfigurationError(
45-
f"Propagator '{name}' not found. "
46-
"It may not be installed or may be misspelled."
47-
)
48-
return ep.load()()
49-
except ConfigurationError:
50-
raise
51-
except Exception as exc:
52-
raise ConfigurationError(
53-
f"Failed to load propagator '{name}': {exc}"
54-
) from exc
36+
"""Load and instantiate a propagator by name."""
37+
return load_entry_point("opentelemetry_propagator", name)()
5538

5639

5740
def _propagators_from_textmap_config(

opentelemetry-sdk/tests/_configuration/test_common.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@
1414

1515
import unittest
1616
from types import SimpleNamespace
17+
from unittest.mock import MagicMock, patch
1718

18-
from opentelemetry.sdk._configuration._common import _parse_headers
19+
from opentelemetry.sdk._configuration._common import (
20+
_parse_headers,
21+
load_entry_point,
22+
)
23+
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
1924

2025

2126
class TestParseHeaders(unittest.TestCase):
@@ -79,3 +84,56 @@ def test_struct_headers_override_headers_list(self):
7984

8085
def test_both_empty_struct_and_none_list_returns_empty_dict(self):
8186
self.assertEqual(_parse_headers([], None), {})
87+
88+
89+
class TestLoadEntryPoint(unittest.TestCase):
90+
def test_returns_loaded_class(self):
91+
mock_class = MagicMock()
92+
mock_ep = MagicMock()
93+
mock_ep.load.return_value = mock_class
94+
with patch(
95+
"opentelemetry.sdk._configuration._common.entry_points",
96+
return_value=[mock_ep],
97+
):
98+
result = load_entry_point("some_group", "some_name")
99+
self.assertIs(result, mock_class)
100+
101+
def test_raises_when_not_found(self):
102+
with patch(
103+
"opentelemetry.sdk._configuration._common.entry_points",
104+
return_value=[],
105+
):
106+
with self.assertRaises(ConfigurationError) as ctx:
107+
load_entry_point("some_group", "missing")
108+
self.assertIn("missing", str(ctx.exception))
109+
self.assertIn("some_group", str(ctx.exception))
110+
111+
def test_wraps_load_exception_in_configuration_error(self):
112+
mock_ep = MagicMock()
113+
mock_ep.load.side_effect = ImportError("bad import")
114+
with patch(
115+
"opentelemetry.sdk._configuration._common.entry_points",
116+
return_value=[mock_ep],
117+
):
118+
with self.assertRaises(ConfigurationError) as ctx:
119+
load_entry_point("some_group", "some_name")
120+
self.assertIn("bad import", str(ctx.exception))
121+
122+
def test_instantiation_error_not_wrapped(self):
123+
"""load_entry_point returns the class; instantiation is the caller's
124+
responsibility. Errors from calling the returned class are NOT wrapped
125+
in ConfigurationError — they propagate as-is."""
126+
mock_class = MagicMock(side_effect=TypeError("bad init"))
127+
mock_ep = MagicMock()
128+
mock_ep.load.return_value = mock_class
129+
with patch(
130+
"opentelemetry.sdk._configuration._common.entry_points",
131+
return_value=[mock_ep],
132+
):
133+
cls = load_entry_point("some_group", "some_name")
134+
# load_entry_point itself succeeds
135+
self.assertIs(cls, mock_class)
136+
# Calling the returned class raises the original error, not
137+
# ConfigurationError
138+
with self.assertRaises(TypeError, msg="bad init"):
139+
cls()

opentelemetry-sdk/tests/_configuration/test_propagator.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_b3_via_entry_point(self):
8989
mock_ep.load.return_value = lambda: mock_propagator
9090

9191
with patch(
92-
"opentelemetry.sdk._configuration._propagator.entry_points",
92+
"opentelemetry.sdk._configuration._common.entry_points",
9393
return_value=[mock_ep],
9494
):
9595
config = PropagatorConfig(
@@ -106,7 +106,7 @@ def test_b3multi_via_entry_point(self):
106106
mock_ep.load.return_value = lambda: mock_propagator
107107

108108
with patch(
109-
"opentelemetry.sdk._configuration._propagator.entry_points",
109+
"opentelemetry.sdk._configuration._common.entry_points",
110110
return_value=[mock_ep],
111111
):
112112
config = PropagatorConfig(
@@ -118,7 +118,7 @@ def test_b3multi_via_entry_point(self):
118118

119119
def test_b3_not_installed_raises_configuration_error(self):
120120
with patch(
121-
"opentelemetry.sdk._configuration._propagator.entry_points",
121+
"opentelemetry.sdk._configuration._common.entry_points",
122122
return_value=[],
123123
):
124124
config = PropagatorConfig(
@@ -135,7 +135,7 @@ def test_composite_list_tracecontext(self):
135135
mock_ep.load.return_value = lambda: mock_tc
136136

137137
with patch(
138-
"opentelemetry.sdk._configuration._propagator.entry_points",
138+
"opentelemetry.sdk._configuration._common.entry_points",
139139
return_value=[mock_ep],
140140
):
141141
result = create_propagator(config)
@@ -158,7 +158,7 @@ def fake_entry_points(group, name):
158158
return []
159159

160160
with patch(
161-
"opentelemetry.sdk._configuration._propagator.entry_points",
161+
"opentelemetry.sdk._configuration._common.entry_points",
162162
side_effect=fake_entry_points,
163163
):
164164
config = PropagatorConfig(composite_list="tracecontext,baggage")
@@ -182,7 +182,7 @@ def test_composite_list_whitespace_around_names(self):
182182
mock_ep.load.return_value = lambda: mock_tc
183183

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

197197
with patch(
198-
"opentelemetry.sdk._configuration._propagator.entry_points",
198+
"opentelemetry.sdk._configuration._common.entry_points",
199199
return_value=[mock_ep],
200200
):
201201
config = PropagatorConfig(composite_list="broken-prop")
@@ -210,7 +210,7 @@ def test_deduplication_across_composite_and_composite_list(self):
210210
mock_ep.load.return_value = lambda: mock_tc
211211

212212
with patch(
213-
"opentelemetry.sdk._configuration._propagator.entry_points",
213+
"opentelemetry.sdk._configuration._common.entry_points",
214214
return_value=[mock_ep],
215215
):
216216
config = PropagatorConfig(
@@ -229,7 +229,7 @@ def test_deduplication_across_composite_and_composite_list(self):
229229

230230
def test_unknown_composite_list_propagator_raises(self):
231231
with patch(
232-
"opentelemetry.sdk._configuration._propagator.entry_points",
232+
"opentelemetry.sdk._configuration._common.entry_points",
233233
return_value=[],
234234
):
235235
config = PropagatorConfig(composite_list="nonexistent")

0 commit comments

Comments
 (0)