Skip to content

Commit

Permalink
Make opentelemetry_metrics_exporter entrypoint support pull exporte…
Browse files Browse the repository at this point in the history
…rs (#3428)
  • Loading branch information
aabmass committed Sep 15, 2023
1 parent f02029c commit 6973de2
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 11 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
23 changes: 23 additions & 0 deletions opentelemetry-api/src/opentelemetry/environment_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@
"""
.. envvar:: OTEL_METRICS_EXPORTER
Specifies which exporter is used for metrics. See `General SDK Configuration
<https://opentelemetry.io/docs/concepts/sdk-configuration/general-sdk-configuration/#otel_metrics_exporter>`_.
**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"
Expand Down
27 changes: 19 additions & 8 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 @@ -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)
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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")
Expand Down
51 changes: 49 additions & 2 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 Down

0 comments on commit 6973de2

Please sign in to comment.