diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a7a983987..50db7d0bd03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3335](https://github.com/open-telemetry/opentelemetry-python/pull/3335)) - Fix error when no LoggerProvider configured for LoggingHandler ([#3423](https://github.com/open-telemetry/opentelemetry-python/pull/3423)) - +- Make `opentelemetry_metrics_exporter` entrypoint support pull exporters + ([#3428](https://github.com/open-telemetry/opentelemetry-python/pull/3428)) ## Version 1.20.0/0.41b0 (2023-09-04) diff --git a/opentelemetry-api/src/opentelemetry/environment_variables.py b/opentelemetry-api/src/opentelemetry/environment_variables.py index c54b13c6da0..c15b96be14a 100644 --- a/opentelemetry-api/src/opentelemetry/environment_variables.py +++ b/opentelemetry-api/src/opentelemetry/environment_variables.py @@ -22,6 +22,29 @@ """ .. envvar:: OTEL_METRICS_EXPORTER +Specifies which exporter is used for metrics. See `General SDK Configuration +`_. + +**Default value:** ``"otlp"`` + +**Example:** + +``export OTEL_METRICS_EXPORTER="prometheus"`` + +Accepted values for ``OTEL_METRICS_EXPORTER`` are: + +- ``"otlp"`` +- ``"prometheus"`` +- ``"none"``: No automatically configured exporter for metrics. + +.. note:: + + Exporter packages may add entry points for group ``opentelemetry_metrics_exporter`` which + can then be used with this environment variable by name. The entry point should point to + either a `opentelemetry.sdk.metrics.export.MetricExporter` (push exporter) or + `opentelemetry.sdk.metrics.export.MetricReader` (pull exporter) subclass; it must be + constructable without any required arguments. This mechanism is considered experimental and + may change in subsequent releases. """ OTEL_PROPAGATORS = "OTEL_PROPAGATORS" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 958a50394e9..e2abcbefa1f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -21,7 +21,7 @@ import os from abc import ABC, abstractmethod from os import environ -from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type +from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union from typing_extensions import Literal @@ -47,6 +47,7 @@ from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( MetricExporter, + MetricReader, PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import Resource @@ -210,16 +211,24 @@ def _init_tracing( def _init_metrics( - exporters: Dict[str, Type[MetricExporter]], + exporters_or_readers: Dict[ + str, Union[Type[MetricExporter], Type[MetricReader]] + ], resource: Resource = None, ): metric_readers = [] - for _, exporter_class in exporters.items(): + for _, exporter_or_reader_class in exporters_or_readers.items(): exporter_args = {} - metric_readers.append( - PeriodicExportingMetricReader(exporter_class(**exporter_args)) - ) + + if issubclass(exporter_or_reader_class, MetricReader): + metric_readers.append(exporter_or_reader_class(**exporter_args)) + else: + metric_readers.append( + PeriodicExportingMetricReader( + exporter_or_reader_class(**exporter_args) + ) + ) provider = MeterProvider(resource=resource, metric_readers=metric_readers) set_meter_provider(provider) @@ -249,7 +258,7 @@ def _import_exporters( log_exporter_names: Sequence[str], ) -> Tuple[ Dict[str, Type[SpanExporter]], - Dict[str, Type[MetricExporter]], + Dict[str, Union[Type[MetricExporter], Type[MetricReader]]], Dict[str, Type[LogExporter]], ]: trace_exporters = {} @@ -267,7 +276,9 @@ def _import_exporters( for (exporter_name, exporter_impl,) in _import_config_components( metric_exporter_names, "opentelemetry_metrics_exporter" ): - if issubclass(exporter_impl, MetricExporter): + # The metric exporter components may be push MetricExporter or pull exporters which + # subclass MetricReader directly + if issubclass(exporter_impl, (MetricExporter, MetricReader)): metric_exporters[exporter_name] = exporter_impl else: raise RuntimeError(f"{exporter_name} is not a metric exporter") diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index e64e64ade0c..3696dc61b35 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -18,7 +18,7 @@ from os import environ from typing import Dict, Iterable, Optional, Sequence from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch from pytest import raises @@ -158,6 +158,20 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: return True +# MetricReader that can be configured as a pull exporter +class DummyMetricReaderPullExporter(MetricReader): + def _receive_metrics( + self, + metrics: Iterable[Metric], + timeout_millis: float = 10_000, + **kwargs, + ) -> None: + pass + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + return True + + class DummyOTLPMetricExporter: def __init__(self, *args, **kwargs): self.export_called = False @@ -309,7 +323,6 @@ def tearDown(self): environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"} ) def test_trace_init_default(self): - auto_resource = Resource.create( { "telemetry.auto.version": "test-version", @@ -740,6 +753,18 @@ def test_metrics_init_exporter(self): self.assertIsInstance(reader, DummyMetricReader) self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter) + def test_metrics_init_pull_exporter(self): + resource = Resource.create({}) + _init_metrics( + {"dummy_metric_reader": DummyMetricReaderPullExporter}, + resource=resource, + ) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyMeterProvider) + reader = provider._sdk_config.metric_readers[0] + self.assertIsInstance(reader, DummyMetricReaderPullExporter) + class TestExporterNames(TestCase): @patch.dict( @@ -835,6 +860,28 @@ def test_console_exporters(self): ConsoleMetricExporter.__class__, ) + @patch( + "opentelemetry.sdk._configuration.entry_points", + ) + def test_metric_pull_exporter(self, mock_entry_points: Mock): + def mock_entry_points_impl(group, name): + if name == "dummy_pull_exporter": + return [ + IterEntryPoint( + name=name, class_type=DummyMetricReaderPullExporter + ) + ] + return [] + + mock_entry_points.side_effect = mock_entry_points_impl + _, metric_exporters, _ = _import_exporters( + [], ["dummy_pull_exporter"], [] + ) + self.assertIs( + metric_exporters["dummy_pull_exporter"], + DummyMetricReaderPullExporter, + ) + class TestImportConfigComponents(TestCase): @patch(