diff --git a/CHANGELOG.md b/CHANGELOG.md index 6024431107..a84571cd4f 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 exporter plugin loading to declarative file configuration for all three signals (traces, metrics, logs) via the `opentelemetry_*_exporter` entry point groups + ([#5069](https://github.com/open-telemetry/opentelemetry-python/pull/5069)) - `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/_common.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py index 0498a19e13..92c1665419 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_common.py @@ -73,5 +73,8 @@ def _parse_headers( ) if headers: for pair in headers: - result[pair.name] = pair.value or "" + if isinstance(pair, dict): + result[pair["name"]] = pair.get("value") or "" + else: + result[pair.name] = pair.value or "" return result diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_logger_provider.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_logger_provider.py index 61673238b7..7156507e3e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_logger_provider.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_logger_provider.py @@ -18,7 +18,10 @@ from typing import Optional from opentelemetry._logs import set_logger_provider -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 ( BatchLogRecordProcessor as BatchLogRecordProcessorConfig, @@ -26,9 +29,6 @@ from opentelemetry.sdk._configuration.models import ( LoggerProvider as LoggerProviderConfig, ) -from opentelemetry.sdk._configuration.models import ( - LogRecordExporter as LogRecordExporterConfig, -) from opentelemetry.sdk._configuration.models import ( LogRecordProcessor as LogRecordProcessorConfig, ) @@ -136,24 +136,28 @@ def _create_otlp_grpc_log_exporter( ) -def _create_log_record_exporter( - config: LogRecordExporterConfig, -) -> LogRecordExporter: - """Create a log record exporter from config.""" - if config.console is not None: - return _create_console_log_exporter() - if config.otlp_http is not None: - return _create_otlp_http_log_exporter(config.otlp_http) - if config.otlp_grpc is not None: - return _create_otlp_grpc_log_exporter(config.otlp_grpc) - if config.otlp_file_development is not None: +_LOG_EXPORTER_REGISTRY: dict = { + "otlp_http": _create_otlp_http_log_exporter, + "otlp_grpc": _create_otlp_grpc_log_exporter, + "console": lambda _: _create_console_log_exporter(), +} + + +def _create_log_record_exporter(config: dict) -> LogRecordExporter: + """Create a log record exporter from a config dict with a single key naming the exporter type. + + Known names (otlp_http, otlp_grpc, console) are bootstrapped directly. + Unknown names are loaded via the ``opentelemetry_logs_exporter`` entry + point group, matching the spec's PluginComponentProvider mechanism. + """ + if len(config) != 1: raise ConfigurationError( - "otlp_file_development log exporter is experimental and not yet supported." + f"Log exporter config must have exactly one key, got: {list(config.keys())}" ) - raise ConfigurationError( - "No exporter type specified in log record exporter config. " - "Supported types: console, otlp_http, otlp_grpc." - ) + name, exporter_config = next(iter(config.items())) + if name in _LOG_EXPORTER_REGISTRY: + return _LOG_EXPORTER_REGISTRY[name](exporter_config) + return load_entry_point("opentelemetry_logs_exporter", name)() def _create_batch_log_record_processor( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_meter_provider.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_meter_provider.py index 257351135f..fa35fd9977 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_meter_provider.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_meter_provider.py @@ -18,7 +18,10 @@ from typing import Optional, Set, Type from opentelemetry import metrics -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 ( Aggregation as AggregationConfig, @@ -49,9 +52,6 @@ from opentelemetry.sdk._configuration.models import ( PeriodicMetricReader as PeriodicMetricReaderConfig, ) -from opentelemetry.sdk._configuration.models import ( - PushMetricExporter as PushMetricExporterConfig, -) from opentelemetry.sdk._configuration.models import ( View as ViewConfig, ) @@ -349,24 +349,28 @@ def _create_otlp_grpc_metric_exporter( ) -def _create_push_metric_exporter( - config: PushMetricExporterConfig, -) -> MetricExporter: - """Create a push metric exporter from config.""" - if config.console is not None: - return _create_console_metric_exporter(config.console) - if config.otlp_http is not None: - return _create_otlp_http_metric_exporter(config.otlp_http) - if config.otlp_grpc is not None: - return _create_otlp_grpc_metric_exporter(config.otlp_grpc) - if config.otlp_file_development is not None: +_METRIC_EXPORTER_REGISTRY: dict = { + "otlp_http": _create_otlp_http_metric_exporter, + "otlp_grpc": _create_otlp_grpc_metric_exporter, + "console": _create_console_metric_exporter, +} + + +def _create_push_metric_exporter(config: dict) -> MetricExporter: + """Create a push metric exporter from a config dict with a single key naming the exporter type. + + Known names (otlp_http, otlp_grpc, console) are bootstrapped directly. + Unknown names are loaded via the ``opentelemetry_metrics_exporter`` entry + point group, matching the spec's PluginComponentProvider mechanism. + """ + if len(config) != 1: raise ConfigurationError( - "otlp_file_development metric exporter is experimental and not yet supported." + f"Metric exporter config must have exactly one key, got: {list(config.keys())}" ) - raise ConfigurationError( - "No exporter type specified in push metric exporter config. " - "Supported types: console, otlp_http, otlp_grpc." - ) + name, exporter_config = next(iter(config.items())) + if name in _METRIC_EXPORTER_REGISTRY: + return _METRIC_EXPORTER_REGISTRY[name](exporter_config) + return load_entry_point("opentelemetry_metrics_exporter", name)() def _create_periodic_metric_reader( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_tracer_provider.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_tracer_provider.py index 32dfd96567..687a0ec10c 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_tracer_provider.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_tracer_provider.py @@ -18,7 +18,10 @@ from typing import 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, @@ -32,9 +35,6 @@ from opentelemetry.sdk._configuration.models import ( Sampler as SamplerConfig, ) -from opentelemetry.sdk._configuration.models import ( - SpanExporter as SpanExporterConfig, -) from opentelemetry.sdk._configuration.models import ( SpanLimits as SpanLimitsConfig, ) @@ -146,18 +146,28 @@ def _create_otlp_grpc_span_exporter( ) -def _create_span_exporter(config: SpanExporterConfig) -> SpanExporter: - """Create a span exporter from config.""" - if config.otlp_http is not None: - return _create_otlp_http_span_exporter(config.otlp_http) - if config.otlp_grpc is not None: - return _create_otlp_grpc_span_exporter(config.otlp_grpc) - if config.console is not None: - return ConsoleSpanExporter() - raise ConfigurationError( - "No exporter type specified in span exporter config. " - "Supported types: otlp_http, otlp_grpc, console." - ) +_SPAN_EXPORTER_REGISTRY: dict = { + "otlp_http": _create_otlp_http_span_exporter, + "otlp_grpc": _create_otlp_grpc_span_exporter, + "console": lambda _: ConsoleSpanExporter(), +} + + +def _create_span_exporter(config: dict) -> SpanExporter: + """Create a span exporter from a config dict with a single key naming the exporter type. + + Known names (otlp_http, otlp_grpc, console) are bootstrapped directly. + Unknown names are loaded via the ``opentelemetry_traces_exporter`` entry + point group, matching the spec's PluginComponentProvider mechanism. + """ + if len(config) != 1: + raise ConfigurationError( + f"Span exporter config must have exactly one key, got: {list(config.keys())}" + ) + name, exporter_config = next(iter(config.items())) + if name in _SPAN_EXPORTER_REGISTRY: + return _SPAN_EXPORTER_REGISTRY[name](exporter_config) + return load_entry_point("opentelemetry_traces_exporter", name)() def _create_span_processor( diff --git a/opentelemetry-sdk/tests/_configuration/test_logger_provider.py b/opentelemetry-sdk/tests/_configuration/test_logger_provider.py index ff820d105a..3de8b5cbf9 100644 --- a/opentelemetry-sdk/tests/_configuration/test_logger_provider.py +++ b/opentelemetry-sdk/tests/_configuration/test_logger_provider.py @@ -39,9 +39,6 @@ from opentelemetry.sdk._configuration.models import ( LoggerProvider as LoggerProviderConfig, ) -from opentelemetry.sdk._configuration.models import ( - LogRecordExporter as LogRecordExporterConfig, -) from opentelemetry.sdk._configuration.models import ( LogRecordLimits as LogRecordLimitsConfig, ) @@ -100,7 +97,7 @@ def _make_batch_config( max_export_batch_size=None, ): if exporter_config is None: - exporter_config = LogRecordExporterConfig(console={}) + exporter_config = {"console": {}} return BatchLogRecordProcessorConfig( exporter=exporter_config, schedule_delay=schedule_delay, @@ -184,9 +181,7 @@ def test_batch_processor_uses_console_exporter(self): ) def test_simple_processor_uses_console_exporter(self): - config = SimpleLogRecordProcessorConfig( - exporter=LogRecordExporterConfig(console={}) - ) + config = SimpleLogRecordProcessorConfig(exporter={"console": {}}) processor = _create_simple_log_record_processor(config) self.assertIsInstance(processor, SimpleLogRecordProcessor) self.assertIsInstance(processor._exporter, ConsoleLogRecordExporter) @@ -198,9 +193,7 @@ def test_batch_processor_dispatched_from_processor_config(self): def test_simple_processor_dispatched_from_processor_config(self): config = LogRecordProcessorConfig( - simple=SimpleLogRecordProcessorConfig( - exporter=LogRecordExporterConfig(console={}) - ) + simple=SimpleLogRecordProcessorConfig(exporter={"console": {}}) ) processor = _create_log_record_processor(config) self.assertIsInstance(processor, SimpleLogRecordProcessor) @@ -224,24 +217,39 @@ def test_batch_processor_suppresses_env_var(self): class TestCreateLogRecordExporters(unittest.TestCase): def test_console_exporter(self): - config = LogRecordExporterConfig(console={}) + config = {"console": {}} exporter = _create_log_record_exporter(config) self.assertIsInstance(exporter, ConsoleLogRecordExporter) - def test_otlp_file_development_raises(self): - config = LogRecordExporterConfig(otlp_file_development={}) - with self.assertRaises(ConfigurationError): - _create_log_record_exporter(config) - def test_no_exporter_type_raises(self): - config = LogRecordExporterConfig() + config = {} with self.assertRaises(ConfigurationError): _create_log_record_exporter(config) + def test_plugin_log_exporter_loaded_via_entry_point(self): + mock_exporter = MagicMock() + mock_class = MagicMock(return_value=mock_exporter) + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[MagicMock(**{"load.return_value": mock_class})], + ): + result = _create_log_record_exporter({"my_custom_exporter": {}}) + self.assertIs(result, mock_exporter) + + def test_unknown_log_exporter_raises_configuration_error(self): + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[], + ): + with self.assertRaises(ConfigurationError): + _create_log_record_exporter({"no_such_exporter": {}}) + def test_otlp_http_missing_package_raises(self): - config = LogRecordExporterConfig( - otlp_http=OtlpHttpExporterConfig(endpoint="http://localhost:4318") - ) + config = { + "otlp_http": OtlpHttpExporterConfig( + endpoint="http://localhost:4318" + ) + } with patch.dict( sys.modules, { @@ -253,9 +261,11 @@ def test_otlp_http_missing_package_raises(self): _create_log_record_exporter(config) def test_otlp_grpc_missing_package_raises(self): - config = LogRecordExporterConfig( - otlp_grpc=OtlpGrpcExporterConfig(endpoint="http://localhost:4317") - ) + config = { + "otlp_grpc": OtlpGrpcExporterConfig( + endpoint="http://localhost:4317" + ) + } with patch.dict( sys.modules, { @@ -283,12 +293,12 @@ def test_otlp_http_exporter_endpoint(self): "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, }, ): - config = LogRecordExporterConfig( - otlp_http=OtlpHttpExporterConfig( + config = { + "otlp_http": OtlpHttpExporterConfig( endpoint="http://collector:4318", timeout=5000, ) - ) + } _create_log_record_exporter(config) mock_exporter_cls.assert_called_once() @@ -311,13 +321,13 @@ def test_otlp_http_exporter_headers(self): "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, }, ): - config = LogRecordExporterConfig( - otlp_http=OtlpHttpExporterConfig( + config = { + "otlp_http": OtlpHttpExporterConfig( headers=[ NameStringValuePair(name="x-api-key", value="secret") ] ) - ) + } _create_log_record_exporter(config) call_kwargs = mock_exporter_cls.call_args.kwargs @@ -337,12 +347,12 @@ def test_otlp_grpc_exporter_endpoint(self): "opentelemetry.exporter.otlp.proto.grpc._log_exporter": mock_grpc_log_module, }, ): - config = LogRecordExporterConfig( - otlp_grpc=OtlpGrpcExporterConfig( + config = { + "otlp_grpc": OtlpGrpcExporterConfig( endpoint="http://collector:4317", timeout=10000, ) - ) + } _create_log_record_exporter(config) mock_exporter_cls.assert_called_once() diff --git a/opentelemetry-sdk/tests/_configuration/test_meter_provider.py b/opentelemetry-sdk/tests/_configuration/test_meter_provider.py index 04d60847f0..94647d8075 100644 --- a/opentelemetry-sdk/tests/_configuration/test_meter_provider.py +++ b/opentelemetry-sdk/tests/_configuration/test_meter_provider.py @@ -63,9 +63,6 @@ from opentelemetry.sdk._configuration.models import ( PeriodicMetricReader as PeriodicMetricReaderConfig, ) -from opentelemetry.sdk._configuration.models import ( - PushMetricExporter as PushMetricExporterConfig, -) from opentelemetry.sdk._configuration.models import ( View as ViewConfig, ) @@ -132,9 +129,7 @@ def test_none_config_does_not_read_interval_env_var(self): readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( - exporter=PushMetricExporterConfig( - console=ConsoleMetricExporterConfig() - ) + exporter={"console": ConsoleMetricExporterConfig()} ) ) ] @@ -188,7 +183,7 @@ def _make_periodic_config(exporter_config, interval=None, timeout=None): def test_console_exporter(self): config = self._make_periodic_config( - PushMetricExporterConfig(console=ConsoleMetricExporterConfig()) + {"console": ConsoleMetricExporterConfig()} ) provider = create_meter_provider(config) reader = provider._sdk_config.metric_readers[0] @@ -197,7 +192,7 @@ def test_console_exporter(self): def test_periodic_reader_default_interval(self): config = self._make_periodic_config( - PushMetricExporterConfig(console=ConsoleMetricExporterConfig()) + {"console": ConsoleMetricExporterConfig()} ) provider = create_meter_provider(config) reader = provider._sdk_config.metric_readers[0] @@ -205,7 +200,7 @@ def test_periodic_reader_default_interval(self): def test_periodic_reader_default_timeout(self): config = self._make_periodic_config( - PushMetricExporterConfig(console=ConsoleMetricExporterConfig()) + {"console": ConsoleMetricExporterConfig()} ) provider = create_meter_provider(config) reader = provider._sdk_config.metric_readers[0] @@ -213,7 +208,7 @@ def test_periodic_reader_default_timeout(self): def test_periodic_reader_explicit_interval(self): config = self._make_periodic_config( - PushMetricExporterConfig(console=ConsoleMetricExporterConfig()), + {"console": ConsoleMetricExporterConfig()}, interval=5000, ) provider = create_meter_provider(config) @@ -222,7 +217,7 @@ def test_periodic_reader_explicit_interval(self): def test_periodic_reader_explicit_timeout(self): config = self._make_periodic_config( - PushMetricExporterConfig(console=ConsoleMetricExporterConfig()), + {"console": ConsoleMetricExporterConfig()}, timeout=10000, ) provider = create_meter_provider(config) @@ -231,7 +226,7 @@ def test_periodic_reader_explicit_timeout(self): def test_otlp_http_missing_package_raises(self): config = self._make_periodic_config( - PushMetricExporterConfig(otlp_http=OtlpHttpMetricExporterConfig()) + {"otlp_http": OtlpHttpMetricExporterConfig()} ) with patch.dict( sys.modules, @@ -260,11 +255,11 @@ def test_otlp_http_created_with_endpoint(self): }, ): config = self._make_periodic_config( - PushMetricExporterConfig( - otlp_http=OtlpHttpMetricExporterConfig( + { + "otlp_http": OtlpHttpMetricExporterConfig( endpoint="http://localhost:4318" ) - ) + } ) create_meter_provider(config) @@ -276,7 +271,7 @@ def test_otlp_http_created_with_endpoint(self): def test_otlp_grpc_missing_package_raises(self): config = self._make_periodic_config( - PushMetricExporterConfig(otlp_grpc=OtlpGrpcMetricExporterConfig()) + {"otlp_grpc": OtlpGrpcMetricExporterConfig()} ) with patch.dict( sys.modules, @@ -302,25 +297,41 @@ def test_no_reader_type_raises(self): create_meter_provider(config) def test_no_exporter_type_raises(self): - config = self._make_periodic_config(PushMetricExporterConfig()) + config = self._make_periodic_config({}) with self.assertRaises(ConfigurationError): create_meter_provider(config) + def test_plugin_metric_exporter_loaded_via_entry_point(self): + mock_exporter = MagicMock() + mock_class = MagicMock(return_value=mock_exporter) + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[MagicMock(**{"load.return_value": mock_class})], + ): + config = self._make_periodic_config({"my_custom_exporter": {}}) + provider = create_meter_provider(config) + self.assertEqual(len(provider._sdk_config.metric_readers), 1) + + def test_unknown_metric_exporter_raises_configuration_error(self): + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[], + ): + config = self._make_periodic_config({"no_such_exporter": {}}) + with self.assertRaises(ConfigurationError): + create_meter_provider(config) + def test_multiple_readers(self): config = MeterProviderConfig( readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( - exporter=PushMetricExporterConfig( - console=ConsoleMetricExporterConfig() - ) + exporter={"console": ConsoleMetricExporterConfig()} ) ), MetricReaderConfig( periodic=PeriodicMetricReaderConfig( - exporter=PushMetricExporterConfig( - console=ConsoleMetricExporterConfig() - ) + exporter={"console": ConsoleMetricExporterConfig()} ) ), ] @@ -336,12 +347,12 @@ def _make_console_config(temporality=None, histogram_agg=None): readers=[ MetricReaderConfig( periodic=PeriodicMetricReaderConfig( - exporter=PushMetricExporterConfig( - console=ConsoleMetricExporterConfig( + exporter={ + "console": ConsoleMetricExporterConfig( temporality_preference=temporality, default_histogram_aggregation=histogram_agg, ) - ) + } ) ) ] diff --git a/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py b/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py index 5caf077cd5..1ed4b5d5cd 100644 --- a/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py +++ b/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py @@ -43,9 +43,6 @@ from opentelemetry.sdk._configuration.models import ( SimpleSpanProcessor as SimpleSpanProcessorConfig, ) -from opentelemetry.sdk._configuration.models import ( - SpanExporter as SpanExporterConfig, -) from opentelemetry.sdk._configuration.models import ( SpanLimits as SpanLimitsConfig, ) @@ -248,7 +245,7 @@ def _make_simple_config(exporter_config): ) def test_console_exporter_batch(self): - config = self._make_batch_config(SpanExporterConfig(console={})) + config = self._make_batch_config({"console": {}}) provider = create_tracer_provider(config) procs = provider._active_span_processor._span_processors self.assertEqual(len(procs), 1) @@ -256,7 +253,7 @@ def test_console_exporter_batch(self): self.assertIsInstance(procs[0].span_exporter, ConsoleSpanExporter) def test_console_exporter_simple(self): - config = self._make_simple_config(SpanExporterConfig(console={})) + config = self._make_simple_config({"console": {}}) provider = create_tracer_provider(config) procs = provider._active_span_processor._span_processors self.assertIsInstance(procs[0], SimpleSpanProcessor) @@ -264,7 +261,7 @@ def test_console_exporter_simple(self): def test_otlp_http_missing_package_raises(self): config = self._make_batch_config( - SpanExporterConfig(otlp_http=OtlpHttpExporterConfig()) + {"otlp_http": OtlpHttpExporterConfig()} ) with patch.dict( sys.modules, @@ -294,11 +291,11 @@ def test_otlp_http_created_with_endpoint(self): }, ): config = self._make_batch_config( - SpanExporterConfig( - otlp_http=OtlpHttpExporterConfig( + { + "otlp_http": OtlpHttpExporterConfig( endpoint="http://localhost:4318" ) - ) + } ) create_tracer_provider(config) @@ -323,11 +320,11 @@ def test_otlp_http_headers_list(self): }, ): config = self._make_batch_config( - SpanExporterConfig( - otlp_http=OtlpHttpExporterConfig( + { + "otlp_http": OtlpHttpExporterConfig( headers_list="x-api-key=secret,env=prod" ) - ) + } ) create_tracer_provider(config) @@ -338,7 +335,7 @@ def test_otlp_http_headers_list(self): def test_otlp_grpc_missing_package_raises(self): config = self._make_batch_config( - SpanExporterConfig(otlp_grpc=OtlpGrpcExporterConfig()) + {"otlp_grpc": OtlpGrpcExporterConfig()} ) with patch.dict( sys.modules, @@ -357,10 +354,32 @@ def test_no_processor_type_raises(self): create_tracer_provider(config) def test_no_exporter_type_raises(self): - config = self._make_batch_config(SpanExporterConfig()) + config = self._make_batch_config({}) with self.assertRaises(ConfigurationError): create_tracer_provider(config) + def test_plugin_span_exporter_loaded_via_entry_point(self): + mock_exporter = MagicMock() + mock_class = MagicMock(return_value=mock_exporter) + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[MagicMock(**{"load.return_value": mock_class})], + ): + config = self._make_batch_config({"my_custom_exporter": {}}) + provider = create_tracer_provider(config) + self.assertEqual( + len(provider._active_span_processor._span_processors), 1 + ) + + def test_unknown_span_exporter_raises_configuration_error(self): + with patch( + "opentelemetry.sdk._configuration._common.entry_points", + return_value=[], + ): + config = self._make_batch_config({"no_such_exporter": {}}) + with self.assertRaises(ConfigurationError): + create_tracer_provider(config) + class TestCreateSpanLimits(unittest.TestCase): # pylint: disable=no-self-use