Skip to content

Commit 1feb287

Browse files
committed
add sampler plugin loading to declarative config via entry points
Sampler and ParentBasedSampler are changed from @DataClass to TypeAlias = dict[str, Any] in models.py. The generated dataclass representation dropped unknown keys, making plugin sampler names unrecoverable before reaching the factory. The dict type preserves the raw YAML key, which is the plugin name. _create_sampler() now has a single code path: extract the single key as the sampler name, look it up in _SAMPLER_REGISTRY (always_on, always_off, trace_id_ratio_based, parent_based), and fall back to load_entry_point("opentelemetry_sampler", name) for unknown names. This matches the spec's PluginComponentProvider mechanism. Assisted-by: Claude Sonnet 4.6
1 parent 09bd216 commit 1feb287

3 files changed

Lines changed: 62 additions & 160 deletions

File tree

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

Lines changed: 36 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from __future__ import annotations
1616

1717
import logging
18-
from typing import Optional
18+
from typing import Any, Optional
1919

2020
from opentelemetry import trace
2121
from opentelemetry.sdk._configuration._common import (
@@ -180,88 +180,46 @@ def _create_span_processor(
180180
)
181181

182182

183-
def _create_sampler(config) -> Sampler:
184-
"""Create a sampler from config.
183+
_SAMPLER_REGISTRY: dict[str, Any] = {
184+
"always_on": lambda _: ALWAYS_ON,
185+
"always_off": lambda _: ALWAYS_OFF,
186+
"trace_id_ratio_based": lambda c: TraceIdRatioBased(
187+
(c or {}).get("ratio", 1.0)
188+
),
189+
"parent_based": lambda c: _create_parent_based_sampler(c or {}),
190+
}
185191

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)
210-
if config.always_on is not None:
211-
return ALWAYS_ON
212-
if config.always_off is not None:
213-
return ALWAYS_OFF
214-
if config.trace_id_ratio_based is not None:
215-
ratio = config.trace_id_ratio_based.ratio
216-
return TraceIdRatioBased(ratio if ratio is not None else 1.0)
217-
if config.parent_based is not None:
218-
return _create_parent_based_sampler(config.parent_based)
219-
raise ConfigurationError(
220-
f"Unknown or unsupported sampler type in config: {config!r}. "
221-
"Supported types: always_on, always_off, trace_id_ratio_based, parent_based."
222-
)
223192

193+
def _create_sampler(config: dict) -> Sampler:
194+
"""Create a sampler from a config dict with a single key naming the sampler type.
224195
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.
196+
Known names (always_on, always_off, trace_id_ratio_based, parent_based) are
197+
bootstrapped directly. Unknown names are looked up via the
198+
``opentelemetry_sampler`` entry point group, matching the spec's
199+
PluginComponentProvider mechanism.
229200
"""
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-
245-
root = (
246-
_create_sampler(config.root) if config.root is not None else ALWAYS_ON
247-
)
248-
kwargs = {"root": root}
249-
if config.remote_parent_sampled is not None:
250-
kwargs["remote_parent_sampled"] = _create_sampler(
251-
config.remote_parent_sampled
252-
)
253-
if config.remote_parent_not_sampled is not None:
254-
kwargs["remote_parent_not_sampled"] = _create_sampler(
255-
config.remote_parent_not_sampled
256-
)
257-
if config.local_parent_sampled is not None:
258-
kwargs["local_parent_sampled"] = _create_sampler(
259-
config.local_parent_sampled
260-
)
261-
if config.local_parent_not_sampled is not None:
262-
kwargs["local_parent_not_sampled"] = _create_sampler(
263-
config.local_parent_not_sampled
201+
if len(config) != 1:
202+
raise ConfigurationError(
203+
f"Sampler config must have exactly one key, got: {list(config.keys())}"
264204
)
205+
name, sampler_config = next(iter(config.items()))
206+
if name in _SAMPLER_REGISTRY:
207+
return _SAMPLER_REGISTRY[name](sampler_config)
208+
return load_entry_point("opentelemetry_sampler", name)()
209+
210+
211+
def _create_parent_based_sampler(config: dict) -> Sampler:
212+
"""Create a ParentBased sampler from a config dict, applying SDK defaults for absent delegates."""
213+
root = _create_sampler(config["root"]) if "root" in config else ALWAYS_ON
214+
kwargs: dict = {"root": root}
215+
for key in (
216+
"remote_parent_sampled",
217+
"remote_parent_not_sampled",
218+
"local_parent_sampled",
219+
"local_parent_not_sampled",
220+
):
221+
if key in config:
222+
kwargs[key] = _create_sampler(config[key])
265223
return ParentBased(**kwargs)
266224

267225

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

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -759,24 +759,13 @@ class ExperimentalJaegerRemoteSampler:
759759
interval: int | None = None
760760

761761

762-
@dataclass
763-
class ParentBasedSampler:
764-
root: Sampler | None = None
765-
remote_parent_sampled: Sampler | None = None
766-
remote_parent_not_sampled: Sampler | None = None
767-
local_parent_sampled: Sampler | None = None
768-
local_parent_not_sampled: Sampler | None = None
769-
770-
771-
@dataclass
772-
class Sampler:
773-
always_off: AlwaysOffSampler | None = None
774-
always_on: AlwaysOnSampler | None = None
775-
composite_development: ExperimentalComposableSampler | None = None
776-
jaeger_remote_development: ExperimentalJaegerRemoteSampler | None = None
777-
parent_based: ParentBasedSampler | None = None
778-
probability_development: ExperimentalProbabilitySampler | None = None
779-
trace_id_ratio_based: TraceIdRatioBasedSampler | None = None
762+
# Diverges from codegen: Sampler and ParentBasedSampler are typed as
763+
# dict[str, Any] rather than dataclasses so that unknown sampler names
764+
# (plugin/custom samplers) are preserved as dict keys through the config
765+
# pipeline. The loader stores nested fields as raw dicts anyway, so the
766+
# typed dataclass representation would drop unknown keys silently.
767+
Sampler: TypeAlias = dict[str, Any]
768+
ParentBasedSampler: TypeAlias = dict[str, Any]
780769

781770

782771
@dataclass

opentelemetry-sdk/tests/_configuration/test_tracer_provider.py

Lines changed: 19 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,6 @@
3434
from opentelemetry.sdk._configuration.models import (
3535
OtlpHttpExporter as OtlpHttpExporterConfig,
3636
)
37-
from opentelemetry.sdk._configuration.models import (
38-
ParentBasedSampler as ParentBasedSamplerConfig,
39-
)
40-
from opentelemetry.sdk._configuration.models import (
41-
Sampler as SamplerConfig,
42-
)
4337
from opentelemetry.sdk._configuration.models import (
4438
SimpleSpanProcessor as SimpleSpanProcessorConfig,
4539
)
@@ -52,9 +46,6 @@
5246
from opentelemetry.sdk._configuration.models import (
5347
SpanProcessor as SpanProcessorConfig,
5448
)
55-
from opentelemetry.sdk._configuration.models import (
56-
TraceIdRatioBasedSampler as TraceIdRatioBasedConfig,
57-
)
5849
from opentelemetry.sdk._configuration.models import (
5950
TracerProvider as TracerProviderConfig,
6051
)
@@ -159,57 +150,47 @@ def _make_provider(sampler_config):
159150
)
160151

161152
def test_always_on(self):
162-
provider = self._make_provider(SamplerConfig(always_on={}))
153+
provider = self._make_provider({"always_on": {}})
163154
self.assertIs(provider.sampler, ALWAYS_ON)
164155

165156
def test_always_off(self):
166-
provider = self._make_provider(SamplerConfig(always_off={}))
157+
provider = self._make_provider({"always_off": {}})
167158
self.assertIs(provider.sampler, ALWAYS_OFF)
168159

169160
def test_trace_id_ratio_based(self):
170161
provider = self._make_provider(
171-
SamplerConfig(
172-
trace_id_ratio_based=TraceIdRatioBasedConfig(ratio=0.5)
173-
)
162+
{"trace_id_ratio_based": {"ratio": 0.5}}
174163
)
175164
self.assertIsInstance(provider.sampler, TraceIdRatioBased)
176165
self.assertAlmostEqual(provider.sampler._rate, 0.5)
177166

178167
def test_trace_id_ratio_based_none_ratio_defaults_to_1(self):
179-
provider = self._make_provider(
180-
SamplerConfig(trace_id_ratio_based=TraceIdRatioBasedConfig())
181-
)
168+
provider = self._make_provider({"trace_id_ratio_based": {}})
182169
self.assertIsInstance(provider.sampler, TraceIdRatioBased)
183170
self.assertAlmostEqual(provider.sampler._rate, 1.0)
184171

185172
def test_parent_based_with_root(self):
186173
provider = self._make_provider(
187-
SamplerConfig(
188-
parent_based=ParentBasedSamplerConfig(
189-
root=SamplerConfig(always_on={})
190-
)
191-
)
174+
{"parent_based": {"root": {"always_on": {}}}}
192175
)
193176
self.assertIsInstance(provider.sampler, ParentBased)
194177

195178
def test_parent_based_no_root_defaults_to_always_on(self):
196-
provider = self._make_provider(
197-
SamplerConfig(parent_based=ParentBasedSamplerConfig())
198-
)
179+
provider = self._make_provider({"parent_based": {}})
199180
self.assertIsInstance(provider.sampler, ParentBased)
200181
self.assertIs(provider.sampler._root, ALWAYS_ON)
201182

202183
def test_parent_based_with_delegate_samplers(self):
203184
provider = self._make_provider(
204-
SamplerConfig(
205-
parent_based=ParentBasedSamplerConfig(
206-
root=SamplerConfig(always_on={}),
207-
remote_parent_sampled=SamplerConfig(always_on={}),
208-
remote_parent_not_sampled=SamplerConfig(always_off={}),
209-
local_parent_sampled=SamplerConfig(always_on={}),
210-
local_parent_not_sampled=SamplerConfig(always_off={}),
211-
)
212-
)
185+
{
186+
"parent_based": {
187+
"root": {"always_on": {}},
188+
"remote_parent_sampled": {"always_on": {}},
189+
"remote_parent_not_sampled": {"always_off": {}},
190+
"local_parent_sampled": {"always_on": {}},
191+
"local_parent_not_sampled": {"always_off": {}},
192+
}
193+
}
213194
)
214195
sampler = provider.sampler
215196
self.assertIsInstance(sampler, ParentBased)
@@ -218,37 +199,11 @@ def test_parent_based_with_delegate_samplers(self):
218199
self.assertIs(sampler._local_parent_sampled, ALWAYS_ON)
219200
self.assertIs(sampler._local_parent_not_sampled, ALWAYS_OFF)
220201

221-
def test_unknown_sampler_raises_configuration_error(self):
202+
def test_multiple_keys_raises_configuration_error(self):
222203
with self.assertRaises(ConfigurationError):
223-
create_tracer_provider(
224-
TracerProviderConfig(processors=[], sampler=SamplerConfig())
225-
)
226-
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)
204+
self._make_provider({"always_on": {}, "always_off": {}})
250205

251-
def test_dict_plugin_sampler_loaded_via_entry_point(self):
206+
def test_plugin_sampler_loaded_via_entry_point(self):
252207
mock_sampler = MagicMock(spec=Sampler)
253208
mock_class = MagicMock(return_value=mock_sampler)
254209
with patch(
@@ -258,7 +213,7 @@ def test_dict_plugin_sampler_loaded_via_entry_point(self):
258213
provider = self._make_provider({"my_custom_sampler": {}})
259214
self.assertIs(provider.sampler, mock_sampler)
260215

261-
def test_dict_unknown_plugin_raises_configuration_error(self):
216+
def test_unknown_plugin_raises_configuration_error(self):
262217
with patch(
263218
"opentelemetry.sdk._configuration._common.entry_points",
264219
return_value=[],

0 commit comments

Comments
 (0)