Skip to content

Commit 6973de2

Browse files
authored
Make opentelemetry_metrics_exporter entrypoint support pull exporters (open-telemetry#3428)
1 parent f02029c commit 6973de2

File tree

4 files changed

+93
-11
lines changed

4 files changed

+93
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
([#3335](https://github.com/open-telemetry/opentelemetry-python/pull/3335))
1212
- Fix error when no LoggerProvider configured for LoggingHandler
1313
([#3423](https://github.com/open-telemetry/opentelemetry-python/pull/3423))
14-
14+
- Make `opentelemetry_metrics_exporter` entrypoint support pull exporters
15+
([#3428](https://github.com/open-telemetry/opentelemetry-python/pull/3428))
1516

1617
## Version 1.20.0/0.41b0 (2023-09-04)
1718

opentelemetry-api/src/opentelemetry/environment_variables.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,29 @@
2222
"""
2323
.. envvar:: OTEL_METRICS_EXPORTER
2424
25+
Specifies which exporter is used for metrics. See `General SDK Configuration
26+
<https://opentelemetry.io/docs/concepts/sdk-configuration/general-sdk-configuration/#otel_metrics_exporter>`_.
27+
28+
**Default value:** ``"otlp"``
29+
30+
**Example:**
31+
32+
``export OTEL_METRICS_EXPORTER="prometheus"``
33+
34+
Accepted values for ``OTEL_METRICS_EXPORTER`` are:
35+
36+
- ``"otlp"``
37+
- ``"prometheus"``
38+
- ``"none"``: No automatically configured exporter for metrics.
39+
40+
.. note::
41+
42+
Exporter packages may add entry points for group ``opentelemetry_metrics_exporter`` which
43+
can then be used with this environment variable by name. The entry point should point to
44+
either a `opentelemetry.sdk.metrics.export.MetricExporter` (push exporter) or
45+
`opentelemetry.sdk.metrics.export.MetricReader` (pull exporter) subclass; it must be
46+
constructable without any required arguments. This mechanism is considered experimental and
47+
may change in subsequent releases.
2548
"""
2649

2750
OTEL_PROPAGATORS = "OTEL_PROPAGATORS"

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import os
2222
from abc import ABC, abstractmethod
2323
from os import environ
24-
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type
24+
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
2525

2626
from typing_extensions import Literal
2727

@@ -47,6 +47,7 @@
4747
from opentelemetry.sdk.metrics import MeterProvider
4848
from opentelemetry.sdk.metrics.export import (
4949
MetricExporter,
50+
MetricReader,
5051
PeriodicExportingMetricReader,
5152
)
5253
from opentelemetry.sdk.resources import Resource
@@ -210,16 +211,24 @@ def _init_tracing(
210211

211212

212213
def _init_metrics(
213-
exporters: Dict[str, Type[MetricExporter]],
214+
exporters_or_readers: Dict[
215+
str, Union[Type[MetricExporter], Type[MetricReader]]
216+
],
214217
resource: Resource = None,
215218
):
216219
metric_readers = []
217220

218-
for _, exporter_class in exporters.items():
221+
for _, exporter_or_reader_class in exporters_or_readers.items():
219222
exporter_args = {}
220-
metric_readers.append(
221-
PeriodicExportingMetricReader(exporter_class(**exporter_args))
222-
)
223+
224+
if issubclass(exporter_or_reader_class, MetricReader):
225+
metric_readers.append(exporter_or_reader_class(**exporter_args))
226+
else:
227+
metric_readers.append(
228+
PeriodicExportingMetricReader(
229+
exporter_or_reader_class(**exporter_args)
230+
)
231+
)
223232

224233
provider = MeterProvider(resource=resource, metric_readers=metric_readers)
225234
set_meter_provider(provider)
@@ -249,7 +258,7 @@ def _import_exporters(
249258
log_exporter_names: Sequence[str],
250259
) -> Tuple[
251260
Dict[str, Type[SpanExporter]],
252-
Dict[str, Type[MetricExporter]],
261+
Dict[str, Union[Type[MetricExporter], Type[MetricReader]]],
253262
Dict[str, Type[LogExporter]],
254263
]:
255264
trace_exporters = {}
@@ -267,7 +276,9 @@ def _import_exporters(
267276
for (exporter_name, exporter_impl,) in _import_config_components(
268277
metric_exporter_names, "opentelemetry_metrics_exporter"
269278
):
270-
if issubclass(exporter_impl, MetricExporter):
279+
# The metric exporter components may be push MetricExporter or pull exporters which
280+
# subclass MetricReader directly
281+
if issubclass(exporter_impl, (MetricExporter, MetricReader)):
271282
metric_exporters[exporter_name] = exporter_impl
272283
else:
273284
raise RuntimeError(f"{exporter_name} is not a metric exporter")

opentelemetry-sdk/tests/test_configurator.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from os import environ
1919
from typing import Dict, Iterable, Optional, Sequence
2020
from unittest import TestCase
21-
from unittest.mock import patch
21+
from unittest.mock import Mock, patch
2222

2323
from pytest import raises
2424

@@ -158,6 +158,20 @@ def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
158158
return True
159159

160160

161+
# MetricReader that can be configured as a pull exporter
162+
class DummyMetricReaderPullExporter(MetricReader):
163+
def _receive_metrics(
164+
self,
165+
metrics: Iterable[Metric],
166+
timeout_millis: float = 10_000,
167+
**kwargs,
168+
) -> None:
169+
pass
170+
171+
def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None:
172+
return True
173+
174+
161175
class DummyOTLPMetricExporter:
162176
def __init__(self, *args, **kwargs):
163177
self.export_called = False
@@ -309,7 +323,6 @@ def tearDown(self):
309323
environ, {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-test-service"}
310324
)
311325
def test_trace_init_default(self):
312-
313326
auto_resource = Resource.create(
314327
{
315328
"telemetry.auto.version": "test-version",
@@ -740,6 +753,18 @@ def test_metrics_init_exporter(self):
740753
self.assertIsInstance(reader, DummyMetricReader)
741754
self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter)
742755

756+
def test_metrics_init_pull_exporter(self):
757+
resource = Resource.create({})
758+
_init_metrics(
759+
{"dummy_metric_reader": DummyMetricReaderPullExporter},
760+
resource=resource,
761+
)
762+
self.assertEqual(self.set_provider_mock.call_count, 1)
763+
provider = self.set_provider_mock.call_args[0][0]
764+
self.assertIsInstance(provider, DummyMeterProvider)
765+
reader = provider._sdk_config.metric_readers[0]
766+
self.assertIsInstance(reader, DummyMetricReaderPullExporter)
767+
743768

744769
class TestExporterNames(TestCase):
745770
@patch.dict(
@@ -835,6 +860,28 @@ def test_console_exporters(self):
835860
ConsoleMetricExporter.__class__,
836861
)
837862

863+
@patch(
864+
"opentelemetry.sdk._configuration.entry_points",
865+
)
866+
def test_metric_pull_exporter(self, mock_entry_points: Mock):
867+
def mock_entry_points_impl(group, name):
868+
if name == "dummy_pull_exporter":
869+
return [
870+
IterEntryPoint(
871+
name=name, class_type=DummyMetricReaderPullExporter
872+
)
873+
]
874+
return []
875+
876+
mock_entry_points.side_effect = mock_entry_points_impl
877+
_, metric_exporters, _ = _import_exporters(
878+
[], ["dummy_pull_exporter"], []
879+
)
880+
self.assertIs(
881+
metric_exporters["dummy_pull_exporter"],
882+
DummyMetricReaderPullExporter,
883+
)
884+
838885

839886
class TestImportConfigComponents(TestCase):
840887
@patch(

0 commit comments

Comments
 (0)