Skip to content

Commit 8c6e48d

Browse files
committed
New APIs to add/remove metric readers at run-time
1 parent 82128af commit 8c6e48d

9 files changed

Lines changed: 213 additions & 61 deletions

File tree

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -501,11 +501,12 @@ def __init__(
501501
)
502502
),
503503
resource=resource,
504-
metric_readers=metric_readers,
505504
views=views,
506505
)
506+
self._metric_readers = metric_readers
507507
self._measurement_consumer = SynchronousMeasurementConsumer(
508-
sdk_config=self._sdk_config
508+
sdk_config=self._sdk_config,
509+
metric_readers=metric_readers,
509510
)
510511
disabled = environ.get(OTEL_SDK_DISABLED, "")
511512
self._disabled = disabled.lower().strip() == "true"
@@ -520,7 +521,7 @@ def __init__(
520521
_meter_configurator or _default_meter_configurator
521522
)
522523

523-
for metric_reader in self._sdk_config.metric_readers:
524+
for metric_reader in self._metric_readers:
524525
with self._all_metric_readers_lock:
525526
if metric_reader in self._all_metric_readers:
526527
# pylint: disable=broad-exception-raised
@@ -571,7 +572,7 @@ def force_flush(self, timeout_millis: float = 10_000) -> bool:
571572

572573
metric_reader_error = {}
573574

574-
for metric_reader in self._sdk_config.metric_readers:
575+
for metric_reader in self._metric_readers:
575576
current_ts = time_ns()
576577
try:
577578
if current_ts >= deadline_ns:
@@ -616,7 +617,7 @@ def _shutdown():
616617

617618
metric_reader_error = {}
618619

619-
for metric_reader in self._sdk_config.metric_readers:
620+
for metric_reader in self._metric_readers:
620621
current_ts = time_ns()
621622
try:
622623
if current_ts >= deadline_ns:
@@ -686,3 +687,36 @@ def get_meter(
686687
),
687688
)
688689
return self._meters[instrumentation_scope]
690+
691+
def add_metric_reader(
692+
self, metric_reader: "opentelemetry.sdk.metrics.export.MetricReader"
693+
) -> None:
694+
with self._all_metric_readers_lock:
695+
if metric_reader in self._all_metric_readers:
696+
_logger.warning(
697+
"MetricReader '%s' has been registered already!",
698+
metric_reader,
699+
)
700+
return
701+
self._measurement_consumer.add_metric_reader(metric_reader)
702+
# pylint: disable-next=protected-access
703+
metric_reader._set_collect_callback(
704+
self._measurement_consumer.collect
705+
)
706+
self._all_metric_readers.add(metric_reader)
707+
708+
def remove_metric_reader(
709+
self,
710+
metric_reader: "opentelemetry.sdk.metrics.export.MetricReader",
711+
) -> None:
712+
with self._all_metric_readers_lock:
713+
if metric_reader not in self._all_metric_readers:
714+
_logger.warning(
715+
"MetricReader '%s' has not been registered!", metric_reader
716+
)
717+
return
718+
self._measurement_consumer.remove_metric_reader(metric_reader)
719+
# pylint: disable-next=protected-access
720+
metric_reader._set_collect_callback(None)
721+
metric_reader.shutdown()
722+
self._all_metric_readers.remove(metric_reader)

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
# pylint: disable=unused-import
1616

17+
import weakref
1718
from abc import ABC, abstractmethod
1819
from threading import Lock
1920
from time import time_ns
20-
from typing import List, Mapping, Optional
21+
from typing import Iterable, List, Mapping, Optional
2122

2223
# This kind of import is needed to avoid Sphinx errors.
2324
import opentelemetry.sdk.metrics
2425
import opentelemetry.sdk.metrics._internal.instrument
25-
import opentelemetry.sdk.metrics._internal.sdk_configuration
2626
from opentelemetry.metrics._internal.instrument import CallbackOptions
2727
from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError
2828
from opentelemetry.sdk.metrics._internal.measurement import Measurement
@@ -59,10 +59,10 @@ class SynchronousMeasurementConsumer(MeasurementConsumer):
5959
def __init__(
6060
self,
6161
sdk_config: "opentelemetry.sdk.metrics._internal.SdkConfiguration",
62+
metric_readers: Iterable["opentelemetry.sdk.metrics.MetricReader"],
6263
) -> None:
6364
self._lock = Lock()
6465
self._sdk_config = sdk_config
65-
# should never be mutated
6666
self._reader_storages: Mapping[
6767
opentelemetry.sdk.metrics.export.MetricReader, MetricReaderStorage
6868
] = {
@@ -71,7 +71,7 @@ def __init__(
7171
reader._instrument_class_temporality,
7272
reader._instrument_class_aggregation,
7373
)
74-
for reader in sdk_config.metric_readers
74+
for reader in metric_readers
7575
}
7676
self._async_instruments: List[
7777
opentelemetry.sdk.metrics._internal.instrument._Asynchronous
@@ -86,7 +86,9 @@ def consume_measurement(self, measurement: Measurement) -> None:
8686
measurement.context,
8787
)
8888
)
89-
for reader_storage in self._reader_storages.values():
89+
with self._lock:
90+
reader_storages = weakref.WeakSet(self._reader_storages.values())
91+
for reader_storage in reader_storages:
9092
reader_storage.consume_measurement(
9193
measurement, should_sample_exemplar
9294
)
@@ -143,3 +145,23 @@ def collect(
143145
result = self._reader_storages[metric_reader].collect()
144146

145147
return result
148+
149+
def add_metric_reader(
150+
self, metric_reader: "opentelemetry.sdk.metrics.MetricReader"
151+
) -> None:
152+
"""Registers a new metric reader."""
153+
with self._lock:
154+
self._reader_storages[metric_reader] = MetricReaderStorage(
155+
self._sdk_config,
156+
# pylint: disable-next=protected-access
157+
metric_reader._instrument_class_temporality,
158+
# pylint: disable-next=protected-access
159+
metric_reader._instrument_class_aggregation,
160+
)
161+
162+
def remove_metric_reader(
163+
self, metric_reader: "opentelemetry.sdk.metrics.MetricReader"
164+
) -> None:
165+
"""Unregisters the given metric reader."""
166+
with self._lock:
167+
self._reader_storages.pop(metric_reader)

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/sdk_configuration.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,4 @@
2626
class SdkConfiguration:
2727
exemplar_filter: "opentelemetry.sdk.metrics.ExemplarFilter"
2828
resource: "opentelemetry.sdk.resources.Resource"
29-
metric_readers: Sequence["opentelemetry.sdk.metrics.export.MetricReader"]
3029
views: Sequence["opentelemetry.sdk.metrics.view.View"]

opentelemetry-sdk/tests/_configuration/test_meter_provider.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def test_none_config_uses_supplied_resource(self):
110110

111111
def test_none_config_no_readers(self):
112112
provider = create_meter_provider(None)
113-
self.assertEqual(len(provider._sdk_config.metric_readers), 0)
113+
self.assertEqual(len(provider._metric_readers), 0)
114114

115115
def test_none_config_uses_trace_based_exemplar_filter(self):
116116
provider = create_meter_provider(None)
@@ -141,7 +141,7 @@ def test_none_config_does_not_read_interval_env_var(self):
141141
)
142142
with patch.dict(os.environ, {"OTEL_METRIC_EXPORT_INTERVAL": "999999"}):
143143
provider = create_meter_provider(config)
144-
reader = provider._sdk_config.metric_readers[0]
144+
reader = provider._metric_readers[0]
145145
self.assertIsInstance(reader, PeriodicExportingMetricReader)
146146
self.assertEqual(reader._export_interval_millis, 60000.0)
147147

@@ -165,11 +165,6 @@ def test_configure_with_config_sets_global(self):
165165
arg = mock_set.call_args[0][0]
166166
self.assertIsInstance(arg, MeterProvider)
167167

168-
def test_empty_readers_list(self):
169-
config = MeterProviderConfig(readers=[])
170-
provider = create_meter_provider(config)
171-
self.assertEqual(len(provider._sdk_config.metric_readers), 0)
172-
173168

174169
class TestCreateMetricReaders(unittest.TestCase):
175170
@staticmethod
@@ -191,7 +186,7 @@ def test_console_exporter(self):
191186
PushMetricExporterConfig(console=ConsoleMetricExporterConfig())
192187
)
193188
provider = create_meter_provider(config)
194-
reader = provider._sdk_config.metric_readers[0]
189+
reader = provider._metric_readers[0]
195190
self.assertIsInstance(reader, PeriodicExportingMetricReader)
196191
self.assertIsInstance(reader._exporter, ConsoleMetricExporter)
197192

@@ -200,15 +195,15 @@ def test_periodic_reader_default_interval(self):
200195
PushMetricExporterConfig(console=ConsoleMetricExporterConfig())
201196
)
202197
provider = create_meter_provider(config)
203-
reader = provider._sdk_config.metric_readers[0]
198+
reader = provider._metric_readers[0]
204199
self.assertEqual(reader._export_interval_millis, 60000.0)
205200

206201
def test_periodic_reader_default_timeout(self):
207202
config = self._make_periodic_config(
208203
PushMetricExporterConfig(console=ConsoleMetricExporterConfig())
209204
)
210205
provider = create_meter_provider(config)
211-
reader = provider._sdk_config.metric_readers[0]
206+
reader = provider._metric_readers[0]
212207
self.assertEqual(reader._export_timeout_millis, 30000.0)
213208

214209
def test_periodic_reader_explicit_interval(self):
@@ -217,7 +212,7 @@ def test_periodic_reader_explicit_interval(self):
217212
interval=5000,
218213
)
219214
provider = create_meter_provider(config)
220-
reader = provider._sdk_config.metric_readers[0]
215+
reader = provider._metric_readers[0]
221216
self.assertEqual(reader._export_interval_millis, 5000.0)
222217

223218
def test_periodic_reader_explicit_timeout(self):
@@ -226,7 +221,7 @@ def test_periodic_reader_explicit_timeout(self):
226221
timeout=10000,
227222
)
228223
provider = create_meter_provider(config)
229-
reader = provider._sdk_config.metric_readers[0]
224+
reader = provider._metric_readers[0]
230225
self.assertEqual(reader._export_timeout_millis, 10000.0)
231226

232227
def test_otlp_http_missing_package_raises(self):
@@ -326,7 +321,7 @@ def test_multiple_readers(self):
326321
]
327322
)
328323
provider = create_meter_provider(config)
329-
self.assertEqual(len(provider._sdk_config.metric_readers), 2)
324+
self.assertEqual(len(provider._metric_readers), 2)
330325

331326

332327
class TestTemporalityAndAggregation(unittest.TestCase):
@@ -350,7 +345,7 @@ def _make_console_config(temporality=None, histogram_agg=None):
350345
@staticmethod
351346
def _get_exporter(config):
352347
provider = create_meter_provider(config)
353-
return provider._sdk_config.metric_readers[0]._exporter
348+
return provider._metric_readers[0]._exporter
354349

355350
def test_default_temporality_is_cumulative(self):
356351
exporter = self._get_exporter(self._make_console_config())

0 commit comments

Comments
 (0)