Skip to content

Commit

Permalink
Make opentelemetry_metrics_exporter entrypoint support pull exporters
Browse files Browse the repository at this point in the history
  • Loading branch information
aabmass committed Sep 7, 2023
1 parent 6070a0d commit 7cced02
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
31 changes: 19 additions & 12 deletions opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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}'"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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")
Expand Down Expand Up @@ -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)

Expand Down
53 changes: 49 additions & 4 deletions opentelemetry-sdk/tests/test_configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down

0 comments on commit 7cced02

Please sign in to comment.