Repository: open-telemetry/opentelemetry-python

Issue:
Verify prev point is returned for cumulative readers instead of None
This LGTM. For cumulative readers though, we need to make sure they return the previous point value instead of None. For example, with a prometheus exporter:

```
counter.add(10, {"foo": "bar"})
# gets scraped, reports 10 for {foo="bar"}
# ... no measurements

# gets scraped again, should continue to report 10 for {foo="bar"}
# even though there were no measurements.
```

Do you know if that is the case? If not we should open another issue for rc2.

_Originally posted by @aabmass in https://github.com/open-telemetry/opentelemetry-python/pull/2745#pullrequestreview-1003204766_


Discussion:
I am a bit confused, what is a "cumulative reader"? Is it an arbitrary designation or can we find out if a `MetricReader` is cumulative from looking into a certain characteristic of the `MetricReader`?

Resolving patch:
diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py
--- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py
+++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py
@@ -77,7 +77,16 @@
 )
 from prometheus_client.core import Metric as PrometheusMetric
 
+from opentelemetry.sdk.metrics import Counter
+from opentelemetry.sdk.metrics import Histogram as HistogramInstrument
+from opentelemetry.sdk.metrics import (
+    ObservableCounter,
+    ObservableGauge,
+    ObservableUpDownCounter,
+    UpDownCounter,
+)
 from opentelemetry.sdk.metrics.export import (
+    AggregationTemporality,
     Gauge,
     Histogram,
     HistogramDataPoint,
@@ -113,7 +122,17 @@ class PrometheusMetricReader(MetricReader):
     """
 
     def __init__(self, prefix: str = "") -> None:
-        super().__init__()
+
+        super().__init__(
+            preferred_temporality={
+                Counter: AggregationTemporality.CUMULATIVE,
+                UpDownCounter: AggregationTemporality.CUMULATIVE,
+                HistogramInstrument: AggregationTemporality.CUMULATIVE,
+                ObservableCounter: AggregationTemporality.CUMULATIVE,
+                ObservableUpDownCounter: AggregationTemporality.CUMULATIVE,
+                ObservableGauge: AggregationTemporality.CUMULATIVE,
+            }
+        )
         self._collector = _CustomCollector(prefix)
         REGISTRY.register(self._collector)
         self._collector._callback = self.collect


Test patch:
diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py
--- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py
+++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py
@@ -23,6 +23,7 @@
     PrometheusMetricReader,
     _CustomCollector,
 )
+from opentelemetry.sdk.metrics import MeterProvider
 from opentelemetry.sdk.metrics.export import (
     AggregationTemporality,
     Histogram,
@@ -51,7 +52,7 @@ def setUp(self):
     def test_constructor(self):
         """Test the constructor."""
         with self._registry_register_patch:
-            exporter = PrometheusMetricReader("testprefix")
+            exporter = PrometheusMetricReader(prefix="testprefix")
             self.assertEqual(exporter._collector._prefix, "testprefix")
             self.assertTrue(self._mock_registry_register.called)
 
@@ -286,3 +287,16 @@ def test_check_value(self):
         self.assertEqual(collector._check_value(True), "true")
         self.assertEqual(collector._check_value(False), "false")
         self.assertEqual(collector._check_value(None), "null")
+
+    def test_multiple_collection_calls(self):
+
+        metric_reader = PrometheusMetricReader(prefix="prefix")
+        provider = MeterProvider(metric_readers=[metric_reader])
+        meter = provider.get_meter("getting-started", "0.1.2")
+        counter = meter.create_counter("counter")
+        counter.add(1)
+        result_0 = list(metric_reader._collector.collect())
+        result_1 = list(metric_reader._collector.collect())
+        result_2 = list(metric_reader._collector.collect())
+        self.assertEqual(result_0, result_1)
+        self.assertEqual(result_1, result_2)
diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py
--- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py
+++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py
@@ -12,11 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from time import sleep
 from unittest import TestCase
 from unittest.mock import Mock
 
 from opentelemetry.metrics import Observation
-from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.sdk.metrics import Counter, MeterProvider
 from opentelemetry.sdk.metrics.export import (
     AggregationTemporality,
     InMemoryMetricReader,
@@ -106,3 +107,50 @@ def test_integration(self):
             ),
             1,
         )
+
+    def test_cumulative_multiple_collect(self):
+
+        reader = InMemoryMetricReader(
+            preferred_temporality={Counter: AggregationTemporality.CUMULATIVE}
+        )
+        meter = MeterProvider(metric_readers=[reader]).get_meter("test_meter")
+        counter = meter.create_counter("counter1")
+        counter.add(1, attributes={"key": "value"})
+
+        reader.collect()
+
+        number_data_point_0 = list(
+            reader._metrics_data.resource_metrics[0]
+            .scope_metrics[0]
+            .metrics[0]
+            .data.data_points
+        )[0]
+
+        # Windows tests fail without this sleep because both time_unix_nano
+        # values are the same.
+        sleep(0.1)
+        reader.collect()
+
+        number_data_point_1 = list(
+            reader._metrics_data.resource_metrics[0]
+            .scope_metrics[0]
+            .metrics[0]
+            .data.data_points
+        )[0]
+
+        self.assertEqual(
+            number_data_point_0.attributes, number_data_point_1.attributes
+        )
+        self.assertEqual(
+            number_data_point_0.start_time_unix_nano,
+            number_data_point_1.start_time_unix_nano,
+        )
+        self.assertEqual(
+            number_data_point_0.start_time_unix_nano,
+            number_data_point_1.start_time_unix_nano,
+        )
+        self.assertEqual(number_data_point_0.value, number_data_point_1.value)
+        self.assertGreater(
+            number_data_point_1.time_unix_nano,
+            number_data_point_0.time_unix_nano,
+        )