From 7cced0248e28bc724bfd639910c43cd9acdd8ecc Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Thu, 7 Sep 2023 18:57:13 +0000 Subject: [PATCH] Make `opentelemetry_metrics_exporter` entrypoint support pull exporters --- CHANGELOG.md | 2 + .../sdk/_configuration/__init__.py | 31 ++++++----- opentelemetry-sdk/tests/test_configurator.py | 53 +++++++++++++++++-- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f1cd0c79dd..339b7d917c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 958a50394e9..8d1367f0c66 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 @@ -90,7 +91,6 @@ def _import_config_components( selected_components: List[str], entry_point_name: str ) -> Sequence[Tuple[str, object]]: - component_implementations = [] for selected_component in selected_components: @@ -108,13 +108,11 @@ def _import_config_components( ) ) except KeyError: - raise RuntimeError( f"Requested entry point '{entry_point_name}' not found" ) except StopIteration: - raise RuntimeError( f"Requested component '{selected_component}' not found in " f"entry point '{entry_point_name}'" @@ -210,16 +208,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 +255,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 +273,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") @@ -380,7 +388,6 @@ class _BaseConfigurator(ABC): _is_instrumented = False def __new__(cls, *args, **kwargs): - if cls._instance is None: cls._instance = object.__new__(cls, *args, **kwargs) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index e64e64ade0c..c5449cdcd78 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( @@ -844,7 +891,6 @@ class TestImportConfigComponents(TestCase): def test__import_config_components_missing_entry_point( self, mock_entry_points ): - with raises(RuntimeError) as error: _import_config_components(["a", "b", "c"], "name") self.assertEqual( @@ -858,7 +904,6 @@ def test__import_config_components_missing_entry_point( def test__import_config_components_missing_component( self, mock_entry_points ): - with raises(RuntimeError) as error: _import_config_components(["a", "b", "c"], "name") self.assertEqual(