diff --git a/CHANGELOG.md b/CHANGELOG.md index 9416dad9812..f0b9ab2dced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Modify Prometheus exporter to translate non-monotonic Sums into Gauges + ([#3306](https://github.com/open-telemetry/opentelemetry-python/pull/3306)) + + ## Version 1.19.0/0.40b0 (2023-07-13) - Drop `setuptools` runtime requirement. @@ -91,7 +95,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Create a single resource instance ([#3118](https://github.com/open-telemetry/opentelemetry-python/pull/3118)) - ## Version 1.15.0/0.36b0 (2022-12-09) - PeriodicExportingMetricsReader with +Inf interval diff --git a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py index 9ece76755cb..252f240b356 100644 --- 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__] diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index c7ce1afae19..a9ab05d01e9 100644 --- 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 index 895904af03f..ff25b092a66 100644 --- 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,