Repository: open-telemetry/opentelemetry-python

Issue:
Add MeasurementConsumer to metrics SDK
See design doc section on [MeasurementConsumer](https://docs.google.com/document/d/1FOmpGCiZAWTd6a3vcEHBnm1LKp7HAKuD4BBN3YCkymc/edit#heading=h.hm0rndce2pgn). For this issue:

- Create ABC `MeasurementConsumer` interface and an empty implementation of `DefaultMeasurementConsumer` (implementation is tracked in #2328).
- Add a member `measurement_consumer: MeasurementConsumer` on MeterProvider
- Wire instruments to call into its methods (e.g. `Counter.add()` should pass measurement to `measurement_consumer.consume_measurement()`)


Discussion:

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/__init__.py
@@ -40,6 +40,10 @@
     ObservableUpDownCounter,
     UpDownCounter,
 )
+from opentelemetry.sdk._metrics.measurement_consumer import (
+    MeasurementConsumer,
+    SynchronousMeasurementConsumer,
+)
 from opentelemetry.sdk._metrics.metric_reader import MetricReader
 from opentelemetry.sdk.resources import Resource
 from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
@@ -51,48 +55,94 @@ class Meter(APIMeter):
     def __init__(
         self,
         instrumentation_info: InstrumentationInfo,
-        meter_provider: APIMeterProvider,
+        measurement_consumer: MeasurementConsumer,
     ):
         super().__init__(instrumentation_info)
         self._instrumentation_info = instrumentation_info
-        self._meter_provider = meter_provider
+        self._measurement_consumer = measurement_consumer
 
     def create_counter(self, name, unit=None, description=None) -> APICounter:
-        return Counter(self._instrumentation_info, name, unit, description)
+        return Counter(
+            name,
+            self._instrumentation_info,
+            self._measurement_consumer,
+            unit,
+            description,
+        )
 
     def create_up_down_counter(
         self, name, unit=None, description=None
     ) -> APIUpDownCounter:
         return UpDownCounter(
-            self._instrumentation_info, name, unit, description
+            name,
+            self._instrumentation_info,
+            self._measurement_consumer,
+            unit,
+            description,
         )
 
     def create_observable_counter(
         self, name, callback, unit=None, description=None
     ) -> APIObservableCounter:
-        return ObservableCounter(
-            self._instrumentation_info, name, callback, unit, description
+
+        instrument = ObservableCounter(
+            name,
+            self._instrumentation_info,
+            self._measurement_consumer,
+            callback,
+            unit,
+            description,
         )
 
+        self._measurement_consumer.register_asynchronous_instrument(instrument)
+
+        return instrument
+
     def create_histogram(
         self, name, unit=None, description=None
     ) -> APIHistogram:
-        return Histogram(self._instrumentation_info, name, unit, description)
+        return Histogram(
+            name,
+            self._instrumentation_info,
+            self._measurement_consumer,
+            unit,
+            description,
+        )
 
     def create_observable_gauge(
         self, name, callback, unit=None, description=None
     ) -> APIObservableGauge:
-        return ObservableGauge(
-            self._instrumentation_info, name, callback, unit, description
+
+        instrument = ObservableGauge(
+            name,
+            self._instrumentation_info,
+            self._measurement_consumer,
+            callback,
+            unit,
+            description,
         )
 
+        self._measurement_consumer.register_asynchronous_instrument(instrument)
+
+        return instrument
+
     def create_observable_up_down_counter(
         self, name, callback, unit=None, description=None
     ) -> APIObservableUpDownCounter:
-        return ObservableUpDownCounter(
-            self._instrumentation_info, name, callback, unit, description
+
+        instrument = ObservableUpDownCounter(
+            name,
+            self._instrumentation_info,
+            self._measurement_consumer,
+            callback,
+            unit,
+            description,
         )
 
+        self._measurement_consumer.register_asynchronous_instrument(instrument)
+
+        return instrument
+
 
 class MeterProvider(APIMeterProvider):
     """See `opentelemetry._metrics.MeterProvider`."""
@@ -106,13 +156,15 @@ def __init__(
         self._lock = Lock()
         self._atexit_handler = None
 
+        self._measurement_consumer = SynchronousMeasurementConsumer()
+
         if shutdown_on_exit:
             self._atexit_handler = register(self.shutdown)
 
         self._metric_readers = metric_readers
 
         for metric_reader in self._metric_readers:
-            metric_reader._register_meter_provider(self)
+            metric_reader._register_measurement_consumer(self)
 
         self._resource = resource
         self._shutdown = False
@@ -169,4 +221,7 @@ def get_meter(
             )
             return _DefaultMeter(name, version=version, schema_url=schema_url)
 
-        return Meter(InstrumentationInfo(name, version, schema_url), self)
+        return Meter(
+            InstrumentationInfo(name, version, schema_url),
+            self._measurement_consumer,
+        )
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py
--- a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py
@@ -30,32 +30,42 @@
 )
 from opentelemetry._metrics.instrument import UpDownCounter as APIUpDownCounter
 from opentelemetry.sdk._metrics.measurement import Measurement
+from opentelemetry.sdk._metrics.measurement_consumer import MeasurementConsumer
 from opentelemetry.sdk.util.instrumentation import InstrumentationInfo
 
 
 class _Synchronous:
     def __init__(
         self,
-        instrumentation_info: InstrumentationInfo,
         name: str,
+        instrumentation_info: InstrumentationInfo,
+        measurement_consumer: MeasurementConsumer,
         unit: str = "",
         description: str = "",
     ):
+        self.name = name
+        self.unit = unit
+        self.description = description
         self._instrumentation_info = instrumentation_info
+        self._measurement_consumer = measurement_consumer
         super().__init__(name, unit=unit, description=description)
 
 
 class _Asynchronous:
     def __init__(
         self,
-        instrumentation_info: InstrumentationInfo,
         name: str,
+        instrumentation_info: InstrumentationInfo,
+        measurement_consumer: MeasurementConsumer,
         callback: CallbackT,
         unit: str = "",
         description: str = "",
     ):
-
+        self.name = name
+        self.unit = unit
+        self.description = description
         self._instrumentation_info = instrumentation_info
+        self._measurement_consumer = measurement_consumer
         super().__init__(name, callback, unit=unit, description=description)
 
         self._callback = callback
@@ -79,12 +89,18 @@ def add(
         if amount < 0:
             raise Exception("amount must be non negative")
 
+        self._measurement_consumer.consume_measurement(
+            Measurement(amount, attributes)
+        )
+
 
 class UpDownCounter(_Synchronous, APIUpDownCounter):
     def add(
         self, amount: Union[int, float], attributes: Dict[str, str] = None
     ):
-        pass
+        self._measurement_consumer.consume_measurement(
+            Measurement(amount, attributes)
+        )
 
 
 class ObservableCounter(_Asynchronous, APIObservableCounter):
@@ -97,7 +113,9 @@ class ObservableUpDownCounter(_Asynchronous, APIObservableUpDownCounter):
 
 class Histogram(_Synchronous, APIHistogram):
     def record(self, amount, attributes=None):
-        pass
+        self._measurement_consumer.consume_measurement(
+            Measurement(amount, attributes)
+        )
 
 
 class ObservableGauge(_Asynchronous, APIObservableGauge):
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py
new file mode 100644
--- /dev/null
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/_metrics/measurement_consumer.py
@@ -0,0 +1,55 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING, Iterable
+
+from opentelemetry.sdk._metrics.aggregation import AggregationTemporality
+from opentelemetry.sdk._metrics.measurement import Measurement
+from opentelemetry.sdk._metrics.metric_reader import MetricReader
+from opentelemetry.sdk._metrics.point import Metric
+
+if TYPE_CHECKING:
+    from opentelemetry.sdk._metrics.instrument import _Asynchronous
+
+
+class MeasurementConsumer(ABC):
+    @abstractmethod
+    def consume_measurement(self, measurement: Measurement) -> None:
+        pass
+
+    @abstractmethod
+    def register_asynchronous_instrument(self, instrument: "_Asynchronous"):
+        pass
+
+    @abstractmethod
+    def collect(
+        self, metric_reader: MetricReader, temporality: AggregationTemporality
+    ) -> Iterable[Metric]:
+        pass
+
+
+class SynchronousMeasurementConsumer(MeasurementConsumer):
+    def consume_measurement(self, measurement: Measurement) -> None:
+        pass
+
+    def register_asynchronous_instrument(
+        self, instrument: "_Asynchronous"
+    ) -> None:
+        pass
+
+    def collect(
+        self, metric_reader: MetricReader, temporality: AggregationTemporality
+    ) -> Iterable[Metric]:
+        pass


Test patch:
diff --git a/opentelemetry-sdk/tests/metrics/test_instrument.py b/opentelemetry-sdk/tests/metrics/test_instrument.py
--- a/opentelemetry-sdk/tests/metrics/test_instrument.py
+++ b/opentelemetry-sdk/tests/metrics/test_instrument.py
@@ -27,7 +27,7 @@ def test_callable_callback(self):
         def callback():
             return [1, 2, 3]
 
-        observable_gauge = ObservableGauge(Mock(), "name", callback)
+        observable_gauge = ObservableGauge("name", Mock(), Mock(), callback)
 
         self.assertEqual(observable_gauge.callback(), [1, 2, 3])
 
@@ -35,7 +35,7 @@ def test_generator_callback(self):
         def callback():
             yield [1, 2, 3]
 
-        observable_gauge = ObservableGauge(Mock(), "name", callback())
+        observable_gauge = ObservableGauge("name", Mock(), Mock(), callback())
 
         self.assertEqual(observable_gauge.callback(), [1, 2, 3])
 
@@ -45,7 +45,9 @@ def test_callable_callback(self):
         def callback():
             return [1, 2, 3]
 
-        observable_counter = ObservableCounter(Mock(), "name", callback)
+        observable_counter = ObservableCounter(
+            "name", Mock(), Mock(), callback
+        )
 
         self.assertEqual(observable_counter.callback(), [1, 2, 3])
 
@@ -53,7 +55,9 @@ def test_generator_callback(self):
         def callback():
             yield [1, 2, 3]
 
-        observable_counter = ObservableCounter(Mock(), "name", callback())
+        observable_counter = ObservableCounter(
+            "name", Mock(), Mock(), callback()
+        )
 
         self.assertEqual(observable_counter.callback(), [1, 2, 3])
 
@@ -64,7 +68,7 @@ def callback():
             return [1, 2, 3]
 
         observable_up_down_counter = ObservableUpDownCounter(
-            Mock(), "name", callback
+            "name", Mock(), Mock(), callback
         )
 
         self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3])
@@ -74,7 +78,7 @@ def callback():
             yield [1, 2, 3]
 
         observable_up_down_counter = ObservableUpDownCounter(
-            Mock(), "name", callback()
+            "name", Mock(), Mock(), callback()
         )
 
         self.assertEqual(observable_up_down_counter.callback(), [1, 2, 3])
diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py
new file mode 100644
--- /dev/null
+++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py
@@ -0,0 +1,99 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from unittest import TestCase
+from unittest.mock import Mock, patch
+
+from opentelemetry.sdk._metrics import MeterProvider
+from opentelemetry.sdk._metrics.measurement_consumer import (
+    MeasurementConsumer,
+    SynchronousMeasurementConsumer,
+)
+
+
+class TestSynchronousMeasurementConsumer(TestCase):
+    def test_parent(self):
+
+        self.assertIsInstance(
+            SynchronousMeasurementConsumer(), MeasurementConsumer
+        )
+
+    @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer")
+    def test_measurement_consumer_class(
+        self, mock_serial_measurement_consumer
+    ):
+        MeterProvider()
+
+        mock_serial_measurement_consumer.assert_called()
+
+    @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer")
+    def test_register_asynchronous_instrument(
+        self, mock_serial_measurement_consumer
+    ):
+
+        meter_provider = MeterProvider()
+
+        meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with(
+            meter_provider.get_meter("name").create_observable_counter(
+                "name", Mock()
+            )
+        )
+        meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with(
+            meter_provider.get_meter("name").create_observable_up_down_counter(
+                "name", Mock()
+            )
+        )
+        meter_provider._measurement_consumer.register_asynchronous_instrument.assert_called_with(
+            meter_provider.get_meter("name").create_observable_gauge(
+                "name", Mock()
+            )
+        )
+
+    @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer")
+    def test_consume_measurement_counter(
+        self, mock_serial_measurement_consumer
+    ):
+
+        meter_provider = MeterProvider()
+        counter = meter_provider.get_meter("name").create_counter("name")
+
+        counter.add(1)
+
+        meter_provider._measurement_consumer.consume_measurement.assert_called()
+
+    @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer")
+    def test_consume_measurement_up_down_counter(
+        self, mock_serial_measurement_consumer
+    ):
+
+        meter_provider = MeterProvider()
+        counter = meter_provider.get_meter("name").create_up_down_counter(
+            "name"
+        )
+
+        counter.add(1)
+
+        meter_provider._measurement_consumer.consume_measurement.assert_called()
+
+    @patch("opentelemetry.sdk._metrics.SynchronousMeasurementConsumer")
+    def test_consume_measurement_histogram(
+        self, mock_serial_measurement_consumer
+    ):
+
+        meter_provider = MeterProvider()
+        counter = meter_provider.get_meter("name").create_histogram("name")
+
+        counter.record(1)
+
+        meter_provider._measurement_consumer.consume_measurement.assert_called()
diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py
--- a/opentelemetry-sdk/tests/metrics/test_metrics.py
+++ b/opentelemetry-sdk/tests/metrics/test_metrics.py
@@ -26,6 +26,9 @@
     ObservableUpDownCounter,
     UpDownCounter,
 )
+from opentelemetry.sdk._metrics.measurement_consumer import (
+    SynchronousMeasurementConsumer,
+)
 from opentelemetry.sdk.resources import Resource
 
 
@@ -79,7 +82,7 @@ def test_shutdown_subsequent_calls(self):
 
 class TestMeter(TestCase):
     def setUp(self):
-        self.meter = Meter(Mock(), MeterProvider())
+        self.meter = Meter(Mock(), SynchronousMeasurementConsumer())
 
     def test_create_counter(self):
         counter = self.meter.create_counter(
@@ -87,6 +90,7 @@ def test_create_counter(self):
         )
 
         self.assertIsInstance(counter, Counter)
+        self.assertEqual(counter.name, "name")
 
     def test_create_up_down_counter(self):
         up_down_counter = self.meter.create_up_down_counter(
@@ -94,6 +98,7 @@ def test_create_up_down_counter(self):
         )
 
         self.assertIsInstance(up_down_counter, UpDownCounter)
+        self.assertEqual(up_down_counter.name, "name")
 
     def test_create_observable_counter(self):
         observable_counter = self.meter.create_observable_counter(
@@ -101,6 +106,7 @@ def test_create_observable_counter(self):
         )
 
         self.assertIsInstance(observable_counter, ObservableCounter)
+        self.assertEqual(observable_counter.name, "name")
 
     def test_create_histogram(self):
         histogram = self.meter.create_histogram(
@@ -108,6 +114,7 @@ def test_create_histogram(self):
         )
 
         self.assertIsInstance(histogram, Histogram)
+        self.assertEqual(histogram.name, "name")
 
     def test_create_observable_gauge(self):
         observable_gauge = self.meter.create_observable_gauge(
@@ -115,6 +122,7 @@ def test_create_observable_gauge(self):
         )
 
         self.assertIsInstance(observable_gauge, ObservableGauge)
+        self.assertEqual(observable_gauge.name, "name")
 
     def test_create_observable_up_down_counter(self):
         observable_up_down_counter = (
@@ -125,3 +133,4 @@ def test_create_observable_up_down_counter(self):
         self.assertIsInstance(
             observable_up_down_counter, ObservableUpDownCounter
         )
+        self.assertEqual(observable_up_down_counter.name, "name")