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 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
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,
)
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,
)
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down Expand Up @@ -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(
Expand Down
74 changes: 42 additions & 32 deletions opentelemetry-sdk/tests/_configuration/test_logger_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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,
{
Expand All @@ -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,
{
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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()
Expand Down
Loading
Loading