Repository: open-telemetry/opentelemetry-python

Issue:
Prometheus exporter should convert non-monotonic sums to gauges
The [current implementation](https://github.com/open-telemetry/opentelemetry-python/blob/main/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py#L255) of Sum export in the prometheus exporter does not differentiate between monotonic and non-monotonic sums.

The [prometheus compatibility spec for sums](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/compatibility/prometheus_and_openmetrics.md#sums) says: `If the aggregation temporality is cumulative and the sum is non-monotonic, it MUST be converted to a Prometheus Gauge.`.


Discussion:

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
@@ -263,7 +263,25 @@ def _translate_to_prometheus(
             for pre_metric_family_id, label_values, value in zip(
                 pre_metric_family_ids, label_valuess, values
             ):
-                if isinstance(metric.data, Sum):
+                is_non_monotonic_sum = (
+                    isinstance(metric.data, Sum)
+                    and metric.data.is_monotonic is False
+                )
+                is_cumulative = (
+                    isinstance(metric.data, Sum)
+                    and metric.data.aggregation_temporality
+                    == AggregationTemporality.CUMULATIVE
+                )
+
+                # The prometheus compatibility spec for sums says: If the aggregation temporality is cumulative and the sum is non-monotonic, it MUST be converted to a Prometheus Gauge.
+                should_convert_sum_to_gauge = (
+                    is_non_monotonic_sum and is_cumulative
+                )
+
+                if (
+                    isinstance(metric.data, Sum)
+                    and not should_convert_sum_to_gauge
+                ):
 
                     metric_family_id = "|".join(
                         [pre_metric_family_id, CounterMetricFamily.__name__]
@@ -281,7 +299,10 @@ def _translate_to_prometheus(
                     metric_family_id_metric_family[
                         metric_family_id
                     ].add_metric(labels=label_values, value=value)
-                elif isinstance(metric.data, Gauge):
+                elif (
+                    isinstance(metric.data, Gauge)
+                    or should_convert_sum_to_gauge
+                ):
 
                     metric_family_id = "|".join(
                         [pre_metric_family_id, GaugeMetricFamily.__name__]


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
@@ -125,10 +125,10 @@ def test_histogram_to_prometheus(self):
             ),
         )
 
-    def test_sum_to_prometheus(self):
+    def test_monotonic_sum_to_prometheus(self):
         labels = {"environment@": "staging", "os": "Windows"}
         metric = _generate_sum(
-            "test@sum",
+            "test@sum_monotonic",
             123,
             attributes=labels,
             description="testdesc",
@@ -156,7 +156,55 @@ def test_sum_to_prometheus(self):
 
         for prometheus_metric in collector.collect():
             self.assertEqual(type(prometheus_metric), CounterMetricFamily)
-            self.assertEqual(prometheus_metric.name, "test_sum_testunit")
+            self.assertEqual(
+                prometheus_metric.name, "test_sum_monotonic_testunit"
+            )
+            self.assertEqual(prometheus_metric.documentation, "testdesc")
+            self.assertTrue(len(prometheus_metric.samples) == 1)
+            self.assertEqual(prometheus_metric.samples[0].value, 123)
+            self.assertTrue(len(prometheus_metric.samples[0].labels) == 2)
+            self.assertEqual(
+                prometheus_metric.samples[0].labels["environment_"], "staging"
+            )
+            self.assertEqual(
+                prometheus_metric.samples[0].labels["os"], "Windows"
+            )
+
+    def test_non_monotonic_sum_to_prometheus(self):
+        labels = {"environment@": "staging", "os": "Windows"}
+        metric = _generate_sum(
+            "test@sum_nonmonotonic",
+            123,
+            attributes=labels,
+            description="testdesc",
+            unit="testunit",
+            is_monotonic=False,
+        )
+
+        metrics_data = MetricsData(
+            resource_metrics=[
+                ResourceMetrics(
+                    resource=Mock(),
+                    scope_metrics=[
+                        ScopeMetrics(
+                            scope=Mock(),
+                            metrics=[metric],
+                            schema_url="schema_url",
+                        )
+                    ],
+                    schema_url="schema_url",
+                )
+            ]
+        )
+
+        collector = _CustomCollector(disable_target_info=True)
+        collector.add_metrics_data(metrics_data)
+
+        for prometheus_metric in collector.collect():
+            self.assertEqual(type(prometheus_metric), GaugeMetricFamily)
+            self.assertEqual(
+                prometheus_metric.name, "test_sum_nonmonotonic_testunit"
+            )
             self.assertEqual(prometheus_metric.documentation, "testdesc")
             self.assertTrue(len(prometheus_metric.samples) == 1)
             self.assertEqual(prometheus_metric.samples[0].value, 123)
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py
@@ -39,7 +39,12 @@ def _generate_metric(
 
 
 def _generate_sum(
-    name, value, attributes=None, description=None, unit=None
+    name,
+    value,
+    attributes=None,
+    description=None,
+    unit=None,
+    is_monotonic=True,
 ) -> Metric:
     if attributes is None:
         attributes = BoundedAttributes(attributes={"a": 1, "b": True})
@@ -55,7 +60,7 @@ def _generate_sum(
                 )
             ],
             aggregation_temporality=AggregationTemporality.CUMULATIVE,
-            is_monotonic=True,
+            is_monotonic=is_monotonic,
         ),
         description=description,
         unit=unit,