diff --git a/CHANGELOG.md b/CHANGELOG.md index 6024431107..06f79b492e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-sdk`: add propagator plugin loading to declarative file configuration via the `opentelemetry_propagator` entry point group, matching the spec's PluginComponentProvider mechanism + ([#5070](https://github.com/open-telemetry/opentelemetry-python/pull/5070)) - `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 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py index 315a4e8bed..2b509a1f99 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py @@ -24,32 +24,33 @@ from opentelemetry.sdk._configuration.models import ( Propagator as PropagatorConfig, ) -from opentelemetry.sdk._configuration.models import ( - TextMapPropagator as TextMapPropagatorConfig, -) from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) - -def _load_entry_point_propagator(name: str) -> TextMapPropagator: - """Load and instantiate a propagator by name.""" - return load_entry_point("opentelemetry_propagator", name)() +# Propagators bundled with the SDK — no entry point lookup needed. +_PROPAGATOR_REGISTRY: dict = { + "tracecontext": lambda _: TraceContextTextMapPropagator(), + "baggage": lambda _: W3CBaggagePropagator(), +} def _propagators_from_textmap_config( - config: TextMapPropagatorConfig, + config: dict, ) -> list[TextMapPropagator]: - """Resolve a single TextMapPropagator config entry to a list of propagators.""" + """Resolve a TextMapPropagator config dict to a list of propagators. + + Each key in the dict names a propagator. Known names (tracecontext, baggage) + are bootstrapped directly. All other names — including b3, b3multi, and + custom plugin propagators — are loaded via the ``opentelemetry_propagator`` + entry point group, matching the spec's PluginComponentProvider mechanism. + """ result: list[TextMapPropagator] = [] - if config.tracecontext is not None: - result.append(TraceContextTextMapPropagator()) - if config.baggage is not None: - result.append(W3CBaggagePropagator()) - if config.b3 is not None: - result.append(_load_entry_point_propagator("b3")) - if config.b3multi is not None: - result.append(_load_entry_point_propagator("b3multi")) + for name, prop_config in config.items(): + if name in _PROPAGATOR_REGISTRY: + result.append(_PROPAGATOR_REGISTRY[name](prop_config)) + else: + result.append(load_entry_point("opentelemetry_propagator", name)()) return result @@ -85,7 +86,7 @@ def create_propagator( name = name.strip() if not name or name.lower() == "none": continue - propagator = _load_entry_point_propagator(name) + propagator = load_entry_point("opentelemetry_propagator", name)() propagators.setdefault(type(propagator), propagator) return CompositePropagator(list(propagators.values())) diff --git a/opentelemetry-sdk/tests/_configuration/test_propagator.py b/opentelemetry-sdk/tests/_configuration/test_propagator.py index d4aab75e74..8f50486668 100644 --- a/opentelemetry-sdk/tests/_configuration/test_propagator.py +++ b/opentelemetry-sdk/tests/_configuration/test_propagator.py @@ -30,9 +30,6 @@ from opentelemetry.sdk._configuration.models import ( Propagator as PropagatorConfig, ) -from opentelemetry.sdk._configuration.models import ( - TextMapPropagator as TextMapPropagatorConfig, -) from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) @@ -50,9 +47,7 @@ def test_empty_config_returns_empty_composite(self): self.assertEqual(result._propagators, []) # type: ignore[attr-defined] def test_tracecontext_only(self): - config = PropagatorConfig( - composite=[TextMapPropagatorConfig(tracecontext={})] - ) + config = PropagatorConfig(composite=[{"tracecontext": {}}]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] self.assertIsInstance( @@ -61,9 +56,7 @@ def test_tracecontext_only(self): ) def test_baggage_only(self): - config = PropagatorConfig( - composite=[TextMapPropagatorConfig(baggage={})] - ) + config = PropagatorConfig(composite=[{"baggage": {}}]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] self.assertIsInstance(result._propagators[0], W3CBaggagePropagator) # type: ignore[attr-defined] @@ -71,8 +64,8 @@ def test_baggage_only(self): def test_tracecontext_and_baggage(self): config = PropagatorConfig( composite=[ - TextMapPropagatorConfig(tracecontext={}), - TextMapPropagatorConfig(baggage={}), + {"tracecontext": {}}, + {"baggage": {}}, ] ) result = create_propagator(config) @@ -92,9 +85,7 @@ def test_b3_via_entry_point(self): "opentelemetry.sdk._configuration._common.entry_points", return_value=[mock_ep], ): - config = PropagatorConfig( - composite=[TextMapPropagatorConfig(b3={})] - ) + config = PropagatorConfig(composite=[{"b3": {}}]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] @@ -109,9 +100,7 @@ def test_b3multi_via_entry_point(self): "opentelemetry.sdk._configuration._common.entry_points", return_value=[mock_ep], ): - config = PropagatorConfig( - composite=[TextMapPropagatorConfig(b3multi={})] - ) + config = PropagatorConfig(composite=[{"b3multi": {}}]) result = create_propagator(config) self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] @@ -121,9 +110,7 @@ def test_b3_not_installed_raises_configuration_error(self): "opentelemetry.sdk._configuration._common.entry_points", return_value=[], ): - config = PropagatorConfig( - composite=[TextMapPropagatorConfig(b3={})] - ) + config = PropagatorConfig(composite=[{"b3": {}}]) with self.assertRaises(ConfigurationError) as ctx: create_propagator(config) self.assertIn("b3", str(ctx.exception)) @@ -214,7 +201,7 @@ def test_deduplication_across_composite_and_composite_list(self): return_value=[mock_ep], ): config = PropagatorConfig( - composite=[TextMapPropagatorConfig(tracecontext={})], + composite=[{"tracecontext": {}}], composite_list="tracecontext", ) result = create_propagator(config) @@ -236,6 +223,30 @@ def test_unknown_composite_list_propagator_raises(self): with self.assertRaises(ConfigurationError): create_propagator(config) + def test_plugin_propagator_via_entry_point(self): + mock_propagator = MagicMock() + mock_ep = MagicMock() + mock_ep.load.return_value = lambda: mock_propagator + + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[mock_ep], + ): + config = PropagatorConfig(composite=[{"my_custom_propagator": {}}]) + result = create_propagator(config) + + self.assertEqual(len(result._propagators), 1) # type: ignore[attr-defined] + self.assertIs(result._propagators[0], mock_propagator) # type: ignore[attr-defined] + + def test_unknown_composite_propagator_raises(self): + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[], + ): + config = PropagatorConfig(composite=[{"nonexistent": {}}]) + with self.assertRaises(ConfigurationError): + create_propagator(config) + class TestConfigurePropagator(unittest.TestCase): def test_configure_propagator_calls_set_global_textmap(self): @@ -248,9 +259,7 @@ def test_configure_propagator_calls_set_global_textmap(self): self.assertIsInstance(arg, CompositePropagator) def test_configure_propagator_with_config(self): - config = PropagatorConfig( - composite=[TextMapPropagatorConfig(tracecontext={})] - ) + config = PropagatorConfig(composite=[{"tracecontext": {}}]) with patch( "opentelemetry.sdk._configuration._propagator.set_global_textmap" ) as mock_set: @@ -263,9 +272,7 @@ def test_configure_propagator_with_config(self): @patch.dict(environ, {OTEL_PROPAGATORS: "baggage"}) def test_otel_propagators_env_var_ignored(self): """OTEL_PROPAGATORS env var must not influence configure_propagator output.""" - config = PropagatorConfig( - composite=[TextMapPropagatorConfig(tracecontext={})] - ) + config = PropagatorConfig(composite=[{"tracecontext": {}}]) with patch( "opentelemetry.sdk._configuration._propagator.set_global_textmap" ) as mock_set: