Skip to content

Commit d4fabb4

Browse files
feat(config): exporter plugin loading via entry points for declarative config (#5128)
1 parent e19d346 commit d4fabb4

8 files changed

Lines changed: 169 additions & 31 deletions

File tree

.changelog/5128.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-sdk`: add exporter plugin loading to declarative file configuration for all three signals (traces, metrics, logs) via the `opentelemetry_*_exporter` entry point groups, matching the spec's PluginComponentProvider mechanism

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,8 @@ def _parse_headers(
9797
)
9898
if headers:
9999
for pair in headers:
100-
result[pair.name] = pair.value or ""
100+
if isinstance(pair, dict):
101+
result[pair["name"]] = pair.get("value") or ""
102+
else:
103+
result[pair.name] = pair.value or ""
101104
return result

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import logging
77

88
from opentelemetry._logs import set_logger_provider
9-
from opentelemetry.sdk._configuration._common import _parse_headers
9+
from opentelemetry.sdk._configuration._common import (
10+
_parse_headers,
11+
load_entry_point,
12+
)
1013
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
1114
from opentelemetry.sdk._configuration.models import (
1215
BatchLogRecordProcessor as BatchLogRecordProcessorConfig,
@@ -124,19 +127,36 @@ def _create_otlp_grpc_log_exporter(
124127
)
125128

126129

130+
_LOG_EXPORTER_REGISTRY: dict = {
131+
"otlp_http": _create_otlp_http_log_exporter,
132+
"otlp_grpc": _create_otlp_grpc_log_exporter,
133+
"console": lambda _: ConsoleLogRecordExporter(),
134+
}
135+
136+
127137
def _create_log_record_exporter(
128138
config: LogRecordExporterConfig,
129139
) -> LogRecordExporter:
130-
"""Create a log record exporter from config."""
131-
if config.console is not None:
132-
return _create_console_log_exporter()
133-
if config.otlp_http is not None:
134-
return _create_otlp_http_log_exporter(config.otlp_http)
135-
if config.otlp_grpc is not None:
136-
return _create_otlp_grpc_log_exporter(config.otlp_grpc)
140+
"""Create a log record exporter from config.
141+
142+
Known exporter types are checked via typed fields on the LogRecordExporter
143+
dataclass. Unknown exporter names captured in additional_properties
144+
by the @_additional_properties decorator are loaded via the
145+
``opentelemetry_logs_exporter`` entry point group.
146+
"""
137147
if config.otlp_file_development is not None:
138148
raise ConfigurationError(
139-
"otlp_file_development log exporter is experimental and not yet supported."
149+
"otlp_file_development log exporter is experimental "
150+
"and not yet supported."
151+
)
152+
for name, factory in _LOG_EXPORTER_REGISTRY.items():
153+
value = getattr(config, name, None)
154+
if value is not None:
155+
return factory(value)
156+
if config.additional_properties:
157+
name, plugin_config = next(iter(config.additional_properties.items()))
158+
return load_entry_point("opentelemetry_logs_exporter", name)(
159+
**(plugin_config or {})
140160
)
141161
raise ConfigurationError(
142162
"No exporter type specified in log record exporter config. "

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

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import logging
77

88
from opentelemetry import metrics
9-
from opentelemetry.sdk._configuration._common import _parse_headers
9+
from opentelemetry.sdk._configuration._common import (
10+
_parse_headers,
11+
load_entry_point,
12+
)
1013
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
1114
from opentelemetry.sdk._configuration.models import (
1215
Aggregation as AggregationConfig,
@@ -337,19 +340,36 @@ def _create_otlp_grpc_metric_exporter(
337340
)
338341

339342

343+
_METRIC_EXPORTER_REGISTRY: dict = {
344+
"otlp_http": _create_otlp_http_metric_exporter,
345+
"otlp_grpc": _create_otlp_grpc_metric_exporter,
346+
"console": _create_console_metric_exporter,
347+
}
348+
349+
340350
def _create_push_metric_exporter(
341351
config: PushMetricExporterConfig,
342352
) -> MetricExporter:
343-
"""Create a push metric exporter from config."""
344-
if config.console is not None:
345-
return _create_console_metric_exporter(config.console)
346-
if config.otlp_http is not None:
347-
return _create_otlp_http_metric_exporter(config.otlp_http)
348-
if config.otlp_grpc is not None:
349-
return _create_otlp_grpc_metric_exporter(config.otlp_grpc)
353+
"""Create a push metric exporter from config.
354+
355+
Known exporter types are checked via typed fields on the PushMetricExporter
356+
dataclass. Unknown exporter names captured in additional_properties
357+
by the @_additional_properties decorator are loaded via the
358+
``opentelemetry_metrics_exporter`` entry point group.
359+
"""
350360
if config.otlp_file_development is not None:
351361
raise ConfigurationError(
352-
"otlp_file_development metric exporter is experimental and not yet supported."
362+
"otlp_file_development metric exporter is experimental "
363+
"and not yet supported."
364+
)
365+
for name, factory in _METRIC_EXPORTER_REGISTRY.items():
366+
value = getattr(config, name, None)
367+
if value is not None:
368+
return factory(value)
369+
if config.additional_properties:
370+
name, plugin_config = next(iter(config.additional_properties.items()))
371+
return load_entry_point("opentelemetry_metrics_exporter", name)(
372+
**(plugin_config or {})
353373
)
354374
raise ConfigurationError(
355375
"No exporter type specified in push metric exporter config. "

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,35 @@ def _create_otlp_grpc_span_exporter(
137137
)
138138

139139

140+
_SPAN_EXPORTER_REGISTRY: dict = {
141+
"otlp_http": _create_otlp_http_span_exporter,
142+
"otlp_grpc": _create_otlp_grpc_span_exporter,
143+
"console": lambda _: ConsoleSpanExporter(),
144+
}
145+
146+
140147
def _create_span_exporter(config: SpanExporterConfig) -> SpanExporter:
141-
"""Create a span exporter from config."""
142-
if config.otlp_http is not None:
143-
return _create_otlp_http_span_exporter(config.otlp_http)
144-
if config.otlp_grpc is not None:
145-
return _create_otlp_grpc_span_exporter(config.otlp_grpc)
146-
if config.console is not None:
147-
return ConsoleSpanExporter()
148+
"""Create a span exporter from config.
149+
150+
Known exporter types are checked via typed fields on the SpanExporter
151+
dataclass. Unknown exporter names captured in additional_properties
152+
by the @_additional_properties decorator are loaded via the
153+
``opentelemetry_traces_exporter`` entry point group.
154+
"""
155+
if config.otlp_file_development is not None:
156+
raise ConfigurationError(
157+
"otlp_file_development span exporter is experimental "
158+
"and not yet supported."
159+
)
160+
for name, factory in _SPAN_EXPORTER_REGISTRY.items():
161+
value = getattr(config, name, None)
162+
if value is not None:
163+
return factory(value)
164+
if config.additional_properties:
165+
name, plugin_config = next(iter(config.additional_properties.items()))
166+
return load_entry_point("opentelemetry_traces_exporter", name)(
167+
**(plugin_config or {})
168+
)
148169
raise ConfigurationError(
149170
"No exporter type specified in span exporter config. "
150171
"Supported types: otlp_http, otlp_grpc, console."

opentelemetry-sdk/tests/_configuration/test_logger_provider.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,16 +217,35 @@ def test_console_exporter(self):
217217
exporter = _create_log_record_exporter(config)
218218
self.assertIsInstance(exporter, ConsoleLogRecordExporter)
219219

220-
def test_otlp_file_development_raises(self):
221-
config = LogRecordExporterConfig(otlp_file_development={})
222-
with self.assertRaises(ConfigurationError):
223-
_create_log_record_exporter(config)
224-
225220
def test_no_exporter_type_raises(self):
226221
config = LogRecordExporterConfig()
227222
with self.assertRaises(ConfigurationError):
228223
_create_log_record_exporter(config)
229224

225+
def test_plugin_log_exporter_loaded_via_entry_point(self):
226+
mock_exporter = MagicMock()
227+
mock_class = MagicMock(return_value=mock_exporter)
228+
with patch(
229+
"opentelemetry.sdk._configuration._common.entry_points",
230+
return_value=[MagicMock(**{"load.return_value": mock_class})],
231+
):
232+
# pylint: disable=unexpected-keyword-arg
233+
result = _create_log_record_exporter(
234+
LogRecordExporterConfig(my_custom_exporter={})
235+
)
236+
self.assertIs(result, mock_exporter)
237+
238+
def test_unknown_log_exporter_raises_configuration_error(self):
239+
with patch(
240+
"opentelemetry.sdk._configuration._common.entry_points",
241+
return_value=[],
242+
):
243+
with self.assertRaises(ConfigurationError):
244+
# pylint: disable=unexpected-keyword-arg
245+
_create_log_record_exporter(
246+
LogRecordExporterConfig(no_such_exporter={})
247+
)
248+
230249
def test_otlp_http_missing_package_raises(self):
231250
config = LogRecordExporterConfig(
232251
otlp_http=OtlpHttpExporterConfig(endpoint="http://localhost:4318")

opentelemetry-sdk/tests/_configuration/test_meter_provider.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,32 @@ def test_no_exporter_type_raises(self):
295295
with self.assertRaises(ConfigurationError):
296296
create_meter_provider(config)
297297

298+
def test_plugin_metric_exporter_loaded_via_entry_point(self):
299+
mock_exporter = MagicMock()
300+
mock_class = MagicMock(return_value=mock_exporter)
301+
with patch(
302+
"opentelemetry.sdk._configuration._common.entry_points",
303+
return_value=[MagicMock(**{"load.return_value": mock_class})],
304+
):
305+
# pylint: disable=unexpected-keyword-arg
306+
config = self._make_periodic_config(
307+
PushMetricExporterConfig(my_custom_exporter={})
308+
)
309+
provider = create_meter_provider(config)
310+
self.assertEqual(len(provider._sdk_config.metric_readers), 1)
311+
312+
def test_unknown_metric_exporter_raises_configuration_error(self):
313+
with patch(
314+
"opentelemetry.sdk._configuration._common.entry_points",
315+
return_value=[],
316+
):
317+
# pylint: disable=unexpected-keyword-arg
318+
config = self._make_periodic_config(
319+
PushMetricExporterConfig(no_such_exporter={})
320+
)
321+
with self.assertRaises(ConfigurationError):
322+
create_meter_provider(config)
323+
298324
def test_multiple_readers(self):
299325
config = MeterProviderConfig(
300326
readers=[

opentelemetry-sdk/tests/_configuration/test_tracer_provider.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,34 @@ def test_no_exporter_type_raises(self):
369369
with self.assertRaises(ConfigurationError):
370370
create_tracer_provider(config)
371371

372+
def test_plugin_span_exporter_loaded_via_entry_point(self):
373+
mock_exporter = MagicMock()
374+
mock_class = MagicMock(return_value=mock_exporter)
375+
with patch(
376+
"opentelemetry.sdk._configuration._common.entry_points",
377+
return_value=[MagicMock(**{"load.return_value": mock_class})],
378+
):
379+
config = self._make_batch_config(
380+
# pylint: disable=unexpected-keyword-arg
381+
SpanExporterConfig(my_custom_exporter={})
382+
)
383+
provider = create_tracer_provider(config)
384+
self.assertEqual(
385+
len(provider._active_span_processor._span_processors), 1
386+
)
387+
388+
def test_unknown_span_exporter_raises_configuration_error(self):
389+
with patch(
390+
"opentelemetry.sdk._configuration._common.entry_points",
391+
return_value=[],
392+
):
393+
config = self._make_batch_config(
394+
# pylint: disable=unexpected-keyword-arg
395+
SpanExporterConfig(no_such_exporter={})
396+
)
397+
with self.assertRaises(ConfigurationError):
398+
create_tracer_provider(config)
399+
372400

373401
class TestCreateSpanLimits(unittest.TestCase):
374402
# pylint: disable=no-self-use

0 commit comments

Comments
 (0)