Skip to content

Commit 09bd216

Browse files
committed
add sampler plugin loading to declarative config via entry points
Extends _create_sampler() to accept raw dicts (from the YAML path) in addition to typed dataclasses (from the direct API path). Unknown sampler names fall back to load_entry_point("opentelemetry_sampler", name), matching the spec's PluginComponentProvider mechanism and Java SDK behaviour. _create_parent_based_sampler() gets the same dict-path treatment so custom samplers can be used as delegate samplers inside parent_based. Assisted-by: Claude Sonnet 4.6
1 parent a3ad87d commit 09bd216

3 files changed

Lines changed: 97 additions & 12 deletions

File tree

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 sampler plugin loading to declarative file configuration via the `opentelemetry_sampler` entry point group, matching the spec's PluginComponentProvider mechanism
16+
([#5071](https://github.com/open-telemetry/opentelemetry-python/pull/5071))
1517
- `opentelemetry-sdk`: add `load_entry_point` shared utility to declarative file configuration for loading plugins via entry points; refactor propagator loading to use it
1618
([#5093](https://github.com/open-telemetry/opentelemetry-python/pull/5093))
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/_tracer_provider.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,17 @@
1818
from typing import Optional
1919

2020
from opentelemetry import trace
21-
from opentelemetry.sdk._configuration._common import _parse_headers
21+
from opentelemetry.sdk._configuration._common import (
22+
_parse_headers,
23+
load_entry_point,
24+
)
2225
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
2326
from opentelemetry.sdk._configuration.models import (
2427
OtlpGrpcExporter as OtlpGrpcExporterConfig,
2528
)
2629
from opentelemetry.sdk._configuration.models import (
2730
OtlpHttpExporter as OtlpHttpExporterConfig,
2831
)
29-
from opentelemetry.sdk._configuration.models import (
30-
ParentBasedSampler as ParentBasedSamplerConfig,
31-
)
32-
from opentelemetry.sdk._configuration.models import (
33-
Sampler as SamplerConfig,
34-
)
3532
from opentelemetry.sdk._configuration.models import (
3633
SpanExporter as SpanExporterConfig,
3734
)
@@ -183,8 +180,33 @@ def _create_span_processor(
183180
)
184181

185182

186-
def _create_sampler(config: SamplerConfig) -> Sampler:
187-
"""Create a sampler from config."""
183+
def _create_sampler(config) -> Sampler:
184+
"""Create a sampler from config.
185+
186+
Accepts either a SamplerConfig dataclass (direct/test usage) or a raw dict
187+
(from the YAML integration path). For unknown sampler names, falls back to
188+
entry point loading via the ``opentelemetry_sampler`` group — matching the
189+
spec's PluginComponentProvider mechanism and Java SDK behaviour.
190+
"""
191+
if isinstance(config, dict):
192+
if len(config) != 1:
193+
raise ConfigurationError(
194+
f"Sampler config must have exactly one key, got: {list(config.keys())}"
195+
)
196+
name, plugin_config = next(iter(config.items()))
197+
known = {
198+
"always_on": lambda _: ALWAYS_ON,
199+
"always_off": lambda _: ALWAYS_OFF,
200+
"trace_id_ratio_based": lambda c: TraceIdRatioBased(
201+
(c or {}).get("ratio", 1.0)
202+
),
203+
"parent_based": lambda c: _create_parent_based_sampler(c or {}),
204+
}
205+
if name in known:
206+
return known[name](plugin_config)
207+
return load_entry_point("opentelemetry_sampler", name)()
208+
209+
# Dataclass path (direct API / unit tests)
188210
if config.always_on is not None:
189211
return ALWAYS_ON
190212
if config.always_off is not None:
@@ -200,12 +222,30 @@ def _create_sampler(config: SamplerConfig) -> Sampler:
200222
)
201223

202224

203-
def _create_parent_based_sampler(config: ParentBasedSamplerConfig) -> Sampler:
204-
"""Create a ParentBased sampler from config, applying SDK defaults for absent delegates."""
225+
def _create_parent_based_sampler(config) -> Sampler:
226+
"""Create a ParentBased sampler from config, applying SDK defaults for absent delegates.
227+
228+
Accepts either a ParentBasedSamplerConfig dataclass or a raw dict.
229+
"""
230+
if isinstance(config, dict):
231+
root = (
232+
_create_sampler(config["root"]) if "root" in config else ALWAYS_ON
233+
)
234+
kwargs: dict = {"root": root}
235+
for key in (
236+
"remote_parent_sampled",
237+
"remote_parent_not_sampled",
238+
"local_parent_sampled",
239+
"local_parent_not_sampled",
240+
):
241+
if key in config:
242+
kwargs[key] = _create_sampler(config[key])
243+
return ParentBased(**kwargs)
244+
205245
root = (
206246
_create_sampler(config.root) if config.root is not None else ALWAYS_ON
207247
)
208-
kwargs: dict = {"root": root}
248+
kwargs = {"root": root}
209249
if config.remote_parent_sampled is not None:
210250
kwargs["remote_parent_sampled"] = _create_sampler(
211251
config.remote_parent_sampled

opentelemetry-sdk/tests/_configuration/test_tracer_provider.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
ALWAYS_OFF,
7070
ALWAYS_ON,
7171
ParentBased,
72+
Sampler,
7273
TraceIdRatioBased,
7374
)
7475

@@ -223,6 +224,48 @@ def test_unknown_sampler_raises_configuration_error(self):
223224
TracerProviderConfig(processors=[], sampler=SamplerConfig())
224225
)
225226

227+
# --- dict path (YAML integration) ---
228+
229+
def test_dict_always_on(self):
230+
provider = self._make_provider({"always_on": {}})
231+
self.assertIs(provider.sampler, ALWAYS_ON)
232+
233+
def test_dict_always_off(self):
234+
provider = self._make_provider({"always_off": {}})
235+
self.assertIs(provider.sampler, ALWAYS_OFF)
236+
237+
def test_dict_trace_id_ratio_based(self):
238+
provider = self._make_provider(
239+
{"trace_id_ratio_based": {"ratio": 0.25}}
240+
)
241+
self.assertIsInstance(provider.sampler, TraceIdRatioBased)
242+
self.assertAlmostEqual(provider.sampler._rate, 0.25)
243+
244+
def test_dict_parent_based(self):
245+
provider = self._make_provider(
246+
{"parent_based": {"root": {"always_off": {}}}}
247+
)
248+
self.assertIsInstance(provider.sampler, ParentBased)
249+
self.assertIs(provider.sampler._root, ALWAYS_OFF)
250+
251+
def test_dict_plugin_sampler_loaded_via_entry_point(self):
252+
mock_sampler = MagicMock(spec=Sampler)
253+
mock_class = MagicMock(return_value=mock_sampler)
254+
with patch(
255+
"opentelemetry.sdk._configuration._common.entry_points",
256+
return_value=[MagicMock(**{"load.return_value": mock_class})],
257+
):
258+
provider = self._make_provider({"my_custom_sampler": {}})
259+
self.assertIs(provider.sampler, mock_sampler)
260+
261+
def test_dict_unknown_plugin_raises_configuration_error(self):
262+
with patch(
263+
"opentelemetry.sdk._configuration._common.entry_points",
264+
return_value=[],
265+
):
266+
with self.assertRaises(ConfigurationError):
267+
self._make_provider({"no_such_sampler": {}})
268+
226269

227270
class TestCreateSpanExporterAndProcessor(unittest.TestCase):
228271
# pylint: disable=no-self-use

0 commit comments

Comments
 (0)