Skip to content
Draft
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 sampler plugin loading to declarative file configuration via the `opentelemetry_sampler` entry point group, matching the spec's PluginComponentProvider mechanism
([#5071](https://github.com/open-telemetry/opentelemetry-python/pull/5071))
- `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`: 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,20 @@
from __future__ import annotations

import logging
from typing import Optional
from typing import Any, Optional

from opentelemetry import trace
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
from opentelemetry.sdk._configuration.models import (
OtlpGrpcExporter as OtlpGrpcExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
OtlpHttpExporter as OtlpHttpExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
ParentBasedSampler as ParentBasedSamplerConfig,
)
from opentelemetry.sdk._configuration.models import (
Sampler as SamplerConfig,
)
from opentelemetry.sdk._configuration.models import (
SpanExporter as SpanExporterConfig,
)
Expand Down Expand Up @@ -183,45 +180,46 @@ def _create_span_processor(
)


def _create_sampler(config: SamplerConfig) -> Sampler:
"""Create a sampler from config."""
if config.always_on is not None:
return ALWAYS_ON
if config.always_off is not None:
return ALWAYS_OFF
if config.trace_id_ratio_based is not None:
ratio = config.trace_id_ratio_based.ratio
return TraceIdRatioBased(ratio if ratio is not None else 1.0)
if config.parent_based is not None:
return _create_parent_based_sampler(config.parent_based)
raise ConfigurationError(
f"Unknown or unsupported sampler type in config: {config!r}. "
"Supported types: always_on, always_off, trace_id_ratio_based, parent_based."
)
_SAMPLER_REGISTRY: dict[str, Any] = {
"always_on": lambda _: ALWAYS_ON,
"always_off": lambda _: ALWAYS_OFF,
"trace_id_ratio_based": lambda c: TraceIdRatioBased(
(c or {}).get("ratio", 1.0)
),
"parent_based": lambda c: _create_parent_based_sampler(c or {}),
}


def _create_parent_based_sampler(config: ParentBasedSamplerConfig) -> Sampler:
"""Create a ParentBased sampler from config, applying SDK defaults for absent delegates."""
root = (
_create_sampler(config.root) if config.root is not None else ALWAYS_ON
)
kwargs: dict = {"root": root}
if config.remote_parent_sampled is not None:
kwargs["remote_parent_sampled"] = _create_sampler(
config.remote_parent_sampled
)
if config.remote_parent_not_sampled is not None:
kwargs["remote_parent_not_sampled"] = _create_sampler(
config.remote_parent_not_sampled
)
if config.local_parent_sampled is not None:
kwargs["local_parent_sampled"] = _create_sampler(
config.local_parent_sampled
)
if config.local_parent_not_sampled is not None:
kwargs["local_parent_not_sampled"] = _create_sampler(
config.local_parent_not_sampled
def _create_sampler(config: dict) -> Sampler:
"""Create a sampler from a config dict with a single key naming the sampler type.

Known names (always_on, always_off, trace_id_ratio_based, parent_based) are
bootstrapped directly. Unknown names are looked up via the
``opentelemetry_sampler`` entry point group, matching the spec's
PluginComponentProvider mechanism.
"""
if len(config) != 1:
raise ConfigurationError(
f"Sampler config must have exactly one key, got: {list(config.keys())}"
)
name, sampler_config = next(iter(config.items()))
if name in _SAMPLER_REGISTRY:
return _SAMPLER_REGISTRY[name](sampler_config)
return load_entry_point("opentelemetry_sampler", name)()


def _create_parent_based_sampler(config: dict) -> Sampler:
"""Create a ParentBased sampler from a config dict, applying SDK defaults for absent delegates."""
root = _create_sampler(config["root"]) if "root" in config else ALWAYS_ON
kwargs: dict = {"root": root}
for key in (
"remote_parent_sampled",
"remote_parent_not_sampled",
"local_parent_sampled",
"local_parent_not_sampled",
):
if key in config:
kwargs[key] = _create_sampler(config[key])
return ParentBased(**kwargs)


Expand Down
74 changes: 36 additions & 38 deletions opentelemetry-sdk/tests/_configuration/test_tracer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@
from opentelemetry.sdk._configuration.models import (
OtlpHttpExporter as OtlpHttpExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
ParentBasedSampler as ParentBasedSamplerConfig,
)
from opentelemetry.sdk._configuration.models import (
Sampler as SamplerConfig,
)
from opentelemetry.sdk._configuration.models import (
SimpleSpanProcessor as SimpleSpanProcessorConfig,
)
Expand All @@ -52,9 +46,6 @@
from opentelemetry.sdk._configuration.models import (
SpanProcessor as SpanProcessorConfig,
)
from opentelemetry.sdk._configuration.models import (
TraceIdRatioBasedSampler as TraceIdRatioBasedConfig,
)
from opentelemetry.sdk._configuration.models import (
TracerProvider as TracerProviderConfig,
)
Expand All @@ -69,6 +60,7 @@
ALWAYS_OFF,
ALWAYS_ON,
ParentBased,
Sampler,
TraceIdRatioBased,
)

Expand Down Expand Up @@ -158,57 +150,47 @@ def _make_provider(sampler_config):
)

def test_always_on(self):
provider = self._make_provider(SamplerConfig(always_on={}))
provider = self._make_provider({"always_on": {}})
self.assertIs(provider.sampler, ALWAYS_ON)

def test_always_off(self):
provider = self._make_provider(SamplerConfig(always_off={}))
provider = self._make_provider({"always_off": {}})
self.assertIs(provider.sampler, ALWAYS_OFF)

def test_trace_id_ratio_based(self):
provider = self._make_provider(
SamplerConfig(
trace_id_ratio_based=TraceIdRatioBasedConfig(ratio=0.5)
)
{"trace_id_ratio_based": {"ratio": 0.5}}
)
self.assertIsInstance(provider.sampler, TraceIdRatioBased)
self.assertAlmostEqual(provider.sampler._rate, 0.5)

def test_trace_id_ratio_based_none_ratio_defaults_to_1(self):
provider = self._make_provider(
SamplerConfig(trace_id_ratio_based=TraceIdRatioBasedConfig())
)
provider = self._make_provider({"trace_id_ratio_based": {}})
self.assertIsInstance(provider.sampler, TraceIdRatioBased)
self.assertAlmostEqual(provider.sampler._rate, 1.0)

def test_parent_based_with_root(self):
provider = self._make_provider(
SamplerConfig(
parent_based=ParentBasedSamplerConfig(
root=SamplerConfig(always_on={})
)
)
{"parent_based": {"root": {"always_on": {}}}}
)
self.assertIsInstance(provider.sampler, ParentBased)

def test_parent_based_no_root_defaults_to_always_on(self):
provider = self._make_provider(
SamplerConfig(parent_based=ParentBasedSamplerConfig())
)
provider = self._make_provider({"parent_based": {}})
self.assertIsInstance(provider.sampler, ParentBased)
self.assertIs(provider.sampler._root, ALWAYS_ON)

def test_parent_based_with_delegate_samplers(self):
provider = self._make_provider(
SamplerConfig(
parent_based=ParentBasedSamplerConfig(
root=SamplerConfig(always_on={}),
remote_parent_sampled=SamplerConfig(always_on={}),
remote_parent_not_sampled=SamplerConfig(always_off={}),
local_parent_sampled=SamplerConfig(always_on={}),
local_parent_not_sampled=SamplerConfig(always_off={}),
)
)
{
"parent_based": {
"root": {"always_on": {}},
"remote_parent_sampled": {"always_on": {}},
"remote_parent_not_sampled": {"always_off": {}},
"local_parent_sampled": {"always_on": {}},
"local_parent_not_sampled": {"always_off": {}},
}
}
)
sampler = provider.sampler
self.assertIsInstance(sampler, ParentBased)
Expand All @@ -217,11 +199,27 @@ def test_parent_based_with_delegate_samplers(self):
self.assertIs(sampler._local_parent_sampled, ALWAYS_ON)
self.assertIs(sampler._local_parent_not_sampled, ALWAYS_OFF)

def test_unknown_sampler_raises_configuration_error(self):
def test_multiple_keys_raises_configuration_error(self):
with self.assertRaises(ConfigurationError):
create_tracer_provider(
TracerProviderConfig(processors=[], sampler=SamplerConfig())
)
self._make_provider({"always_on": {}, "always_off": {}})

def test_plugin_sampler_loaded_via_entry_point(self):
mock_sampler = MagicMock(spec=Sampler)
mock_class = MagicMock(return_value=mock_sampler)
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[MagicMock(**{"load.return_value": mock_class})],
):
provider = self._make_provider({"my_custom_sampler": {}})
self.assertIs(provider.sampler, mock_sampler)

def test_unknown_plugin_raises_configuration_error(self):
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
):
with self.assertRaises(ConfigurationError):
self._make_provider({"no_such_sampler": {}})


class TestCreateSpanExporterAndProcessor(unittest.TestCase):
Expand Down
Loading