From b51a6f8e62a10a4f22455f55e439fe5c5fcac44d Mon Sep 17 00:00:00 2001 From: soundofspace <116737867+soundofspace@users.noreply.github.com> Date: Thu, 18 Apr 2024 00:26:54 +0200 Subject: [PATCH] Sort by label keys before generating labels key and value lists (#3698) * sort by label keys * changelog * test * review * linting * Update contrib SHA --------- Co-authored-by: Leighton Chen Co-authored-by: Diego Hurtado --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 2 ++ .../exporter/prometheus/__init__.py | 2 +- .../tests/test_prometheus_exporter.py | 21 +++++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 651b13ab559..5df83e92ce8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ env: # Otherwise, set variable to the commit of your branch on # opentelemetry-python-contrib which is compatible with these Core repo # changes. - CONTRIB_REPO_SHA: 3c2788469834aa4f5976e1644d757f43d60bc219 + CONTRIB_REPO_SHA: 9ce1c26d2732dfbdadbb492fc38c562dcd08ed2e # This is needed because we do not clone the core repo in contrib builds anymore. # When running contrib builds as part of core builds, we use actions/checkout@v2 which # does not set an environment variable (simply just runs tox), which is different when diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a15c93485a..23077ff4b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3785](https://github.com/open-telemetry/opentelemetry-python/pull/3785)) - Add capture the fully qualified type name for raised exceptions in spans ([#3837](https://github.com/open-telemetry/opentelemetry-python/pull/3837)) +- Prometheus exporter sort label keys to prevent duplicate metrics when user input changes order + ([#3698](https://github.com/open-telemetry/opentelemetry-python/pull/3698)) ## Version 1.24.0/0.45b0 (2024-03-28) 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 b1a8d668c46..6cde540ef6f 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -237,7 +237,7 @@ def _translate_to_prometheus( label_keys = [] label_values = [] - for key, value in number_data_point.attributes.items(): + for key, value in sorted(number_data_point.attributes.items()): label_keys.append(self._sanitize(key)) label_values.append(self._check_value(value)) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index 6d9126c815f..7f0cbaaaeeb 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -436,3 +436,24 @@ def test_target_info_sanitize(self): prometheus_metric.samples[0].labels["ratio"], "0.1", ) + + def test_label_order_does_not_matter(self): + metric_reader = PrometheusMetricReader() + provider = MeterProvider(metric_readers=[metric_reader]) + meter = provider.get_meter("getting-started", "0.1.2") + counter = meter.create_counter("counter") + + counter.add(1, {"cause": "cause1", "reason": "reason1"}) + counter.add(1, {"reason": "reason2", "cause": "cause2"}) + + prometheus_output = generate_latest().decode() + + # All labels are mapped correctly + self.assertIn('cause="cause1"', prometheus_output) + self.assertIn('cause="cause2"', prometheus_output) + self.assertIn('reason="reason1"', prometheus_output) + self.assertIn('reason="reason2"', prometheus_output) + + # Only one metric is generated + metric_count = prometheus_output.count("# HELP counter_total") + self.assertEqual(metric_count, 1)