Skip to content
Open
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 exporter plugin loading to declarative file configuration for all three signals (traces, metrics, logs) via the `opentelemetry_*_exporter` entry point groups
([#5128](https://github.com/open-telemetry/opentelemetry-python/pull/5128))
- Apply fixes for `UP` ruff rule
([#5133](https://github.com/open-telemetry/opentelemetry-python/pull/5133))
- Switch to SPDX license headers and add CI enforcement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import logging

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,
Expand Down Expand Up @@ -124,20 +127,30 @@ def _create_otlp_grpc_log_exporter(
)


_LOG_EXPORTER_REGISTRY: dict = {
"otlp_http": _create_otlp_http_log_exporter,
"otlp_grpc": _create_otlp_grpc_log_exporter,
"console": lambda _: ConsoleLogRecordExporter(),
}


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:
raise ConfigurationError(
"otlp_file_development log exporter is experimental and not yet supported."
)
"""Create a log record exporter from config.

Known exporter types are checked via typed fields on the LogRecordExporter
dataclass. Unknown exporter names captured in additional_properties
Comment on lines +142 to +143
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curiousity/Nit: are "known" vs "unknown" the official terms for declarative config? Overall it works fine, while something like "core"/"standard" vs "custom"/"additional" is just me thinking aloud as "unknown" is sometimes but not always used as a fallback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

known/unknown are not declarative config terms and I agree the naming could be a little friendlier 😄

I've opened #5195 to do the rename across all the plugin loading code once the structural PRs have landed. That way we can batch it in one pass rather than updating each open PR 👍🏻

by the @_additional_properties decorator are loaded via the
``opentelemetry_logs_exporter`` entry point group.
"""
for name, factory in _LOG_EXPORTER_REGISTRY.items():
value = getattr(config, name, None)
if value is not None:
return factory(value)
if config.additional_properties:
name = next(iter(config.additional_properties))
return load_entry_point("opentelemetry_logs_exporter", name)()
raise ConfigurationError(
"No exporter type specified in log record exporter config. "
"Supported types: console, otlp_http, otlp_grpc."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import logging

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,
Expand Down Expand Up @@ -337,20 +340,30 @@ def _create_otlp_grpc_metric_exporter(
)


_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: 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:
raise ConfigurationError(
"otlp_file_development metric exporter is experimental and not yet supported."
)
"""Create a push metric exporter from config.

Known exporter types are checked via typed fields on the PushMetricExporter
dataclass. Unknown exporter names captured in additional_properties
by the @_additional_properties decorator are loaded via the
``opentelemetry_metrics_exporter`` entry point group.
"""
for name, factory in _METRIC_EXPORTER_REGISTRY.items():
value = getattr(config, name, None)
if value is not None:
return factory(value)
if config.additional_properties:
name = next(iter(config.additional_properties))
return load_entry_point("opentelemetry_metrics_exporter", name)()
raise ConfigurationError(
"No exporter type specified in push metric exporter config. "
"Supported types: console, otlp_http, otlp_grpc."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,28 @@ def _create_otlp_grpc_span_exporter(
)


_SPAN_EXPORTER_REGISTRY: dict = {
"otlp_http": _create_otlp_http_span_exporter,
"otlp_grpc": _create_otlp_grpc_span_exporter,
"console": lambda _: ConsoleSpanExporter(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does "console" for this and logger have to be lambda but regular named function for metrics?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traces and logs console exporters have simple constructors with args so a lambda works. Metrics needs a named function because _create_console_metric_exporter processes temporality_preference and default_histogram_aggregation from the config before constructing the exporter.

I also fixed the logs console entry — it was unnecessarily wrapping a helper function. Now both traces and logs use lambda _: Console*Exporter() consistently too.

}


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()
"""Create a span exporter from config.

Known exporter types are checked via typed fields on the SpanExporter
dataclass. Unknown exporter names captured in additional_properties
by the @_additional_properties decorator are loaded via the
``opentelemetry_traces_exporter`` entry point group.
"""
for name, factory in _SPAN_EXPORTER_REGISTRY.items():
value = getattr(config, name, None)
if value is not None:
return factory(value)
if config.additional_properties:
name = next(iter(config.additional_properties))
return load_entry_point("opentelemetry_traces_exporter", name)()
raise ConfigurationError(
"No exporter type specified in span exporter config. "
"Supported types: otlp_http, otlp_grpc, console."
Expand Down
29 changes: 24 additions & 5 deletions opentelemetry-sdk/tests/_configuration/test_logger_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,16 +217,35 @@ def test_console_exporter(self):
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()
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})],
):
# pylint: disable=unexpected-keyword-arg
result = _create_log_record_exporter(
LogRecordExporterConfig(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):
# pylint: disable=unexpected-keyword-arg
_create_log_record_exporter(
LogRecordExporterConfig(no_such_exporter={})
)

def test_otlp_http_missing_package_raises(self):
config = LogRecordExporterConfig(
otlp_http=OtlpHttpExporterConfig(endpoint="http://localhost:4318")
Expand Down
26 changes: 26 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_meter_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,32 @@ def test_no_exporter_type_raises(self):
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})],
):
# pylint: disable=unexpected-keyword-arg
config = self._make_periodic_config(
PushMetricExporterConfig(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=[],
):
# pylint: disable=unexpected-keyword-arg
config = self._make_periodic_config(
PushMetricExporterConfig(no_such_exporter={})
)
with self.assertRaises(ConfigurationError):
create_meter_provider(config)

def test_multiple_readers(self):
config = MeterProviderConfig(
readers=[
Expand Down
28 changes: 28 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_tracer_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,34 @@ def test_no_exporter_type_raises(self):
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(
# pylint: disable=unexpected-keyword-arg
SpanExporterConfig(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(
# pylint: disable=unexpected-keyword-arg
SpanExporterConfig(no_such_exporter={})
)
with self.assertRaises(ConfigurationError):
create_tracer_provider(config)


class TestCreateSpanLimits(unittest.TestCase):
# pylint: disable=no-self-use
Expand Down
Loading