Repository: open-telemetry/opentelemetry-python
Pull request #4521: opentelemetry-sdk: fix explicit histogram aggregation to handle multiple explicit bucket advisories
Author: login: lseguy
Created: 2025-04-01T18:07:36Z | Merged: 2025-04-03T07:05:51Z

# Description

https://github.com/open-telemetry/opentelemetry-python/pull/4434 merged a fix that allowed the `ExplicitBucketHistogramAggregation` created by the OTLP exporter to take into account histogram buckets advice.

However, unlike the `DefaultAggregation`, it does not allow setting independent bucket boundaries for multiple histograms. The first histogram will set the explicit boundaries based on the advice, but the explicit bucket boundaries of further histograms are ignored.

This keeps the behavior of the View's explicit settings taking precedence over the advisory, except when `ExplicitBucketHistogramAggregation` uses the default bucket boundaries.

## Type of change

Please delete options that are not relevant.

- [X] Bug fix (non-breaking change which fixes an issue)

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

- [x] tox -e py311-test-opentelemetry-sdk

# Does This PR Require a Contrib Repo Change?

<!--
Answer the following question based on these examples of changes that would require a Contrib Repo Change:
- [The OTel specification](https://github.com/open-telemetry/opentelemetry-specification) has changed which prompted this PR to update the method interfaces of `opentelemetry-api/` or `opentelemetry-sdk/`
- The method interfaces of `test/util` have changed
- Scripts in `scripts/` that were copied over to the Contrib repo have changed
- Configuration files that were copied over to the Contrib repo have changed (when consistency between repositories is applicable) such as in
    - `pyproject.toml`
    - `isort.cfg`
    - `.flake8`
- When a new `.github/CODEOWNER` is added
- Major changes to project information, such as in:
    - `README.md`
    - `CONTRIBUTING.md`
-->

- [ ] Yes. - Link to PR: 
- [X] No.

# Checklist:

- [x] Followed the style guidelines of this project
- [x] Changelogs have been updated
- [X] Unit tests have been added
- [ ] Documentation has been updated



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07d3121668e..1935034aac0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   ([#4494](https://github.com/open-telemetry/opentelemetry-python/pull/4494))
 - Improve CI by cancelling stale runs and setting timeouts
   ([#4498](https://github.com/open-telemetry/opentelemetry-python/pull/4498))
+- Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories
+ ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521))
 
 ## Version 1.31.0/0.52b0 (2025-03-12)
 
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py
index 8443d9516cf..1779dac0bba 100644
--- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py
+++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py
@@ -1387,18 +1387,17 @@ def _create_aggregation(
                 AggregationTemporality.CUMULATIVE
             )
 
-        if self._boundaries is None:
-            self._boundaries = (
-                instrument._advisory.explicit_bucket_boundaries
-                or _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES
-            )
+        if self._boundaries is not None:
+            boundaries = self._boundaries
+        else:
+            boundaries = instrument._advisory.explicit_bucket_boundaries
 
         return _ExplicitBucketHistogramAggregation(
             attributes,
             instrument_aggregation_temporality,
             start_time_unix_nano,
             reservoir_factory(_ExplicitBucketHistogramAggregation),
-            self._boundaries,
+            boundaries,
             self._record_min_max,
         )
 
diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_histogram_advisory_explicit_buckets.py b/opentelemetry-sdk/tests/metrics/integration_test/test_histogram_advisory_explicit_buckets.py
index 2f46dca87a6..569d7fd1c2c 100644
--- a/opentelemetry-sdk/tests/metrics/integration_test/test_histogram_advisory_explicit_buckets.py
+++ b/opentelemetry-sdk/tests/metrics/integration_test/test_histogram_advisory_explicit_buckets.py
@@ -15,6 +15,9 @@
 from unittest import TestCase
 
 from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.sdk.metrics._internal.aggregation import (
+    _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES,
+)
 from opentelemetry.sdk.metrics._internal.instrument import Histogram
 from opentelemetry.sdk.metrics.export import InMemoryMetricReader
 from opentelemetry.sdk.metrics.view import (
@@ -164,3 +167,78 @@ def test_explicit_aggregation(self):
         self.assertEqual(
             metric.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)
         )
+
+    def test_explicit_aggregation_multiple_histograms(self):
+        reader = InMemoryMetricReader(
+            preferred_aggregation={
+                Histogram: ExplicitBucketHistogramAggregation()
+            }
+        )
+        meter_provider = MeterProvider(
+            metric_readers=[reader],
+        )
+        meter = meter_provider.get_meter("testmeter")
+
+        histogram1 = meter.create_histogram(
+            "testhistogram1",
+            explicit_bucket_boundaries_advisory=[1.0, 2.0, 3.0],
+        )
+        histogram1.record(1, {"label": "value"})
+        histogram1.record(2, {"label": "value"})
+        histogram1.record(3, {"label": "value"})
+
+        histogram2 = meter.create_histogram(
+            "testhistogram2",
+            explicit_bucket_boundaries_advisory=[4.0, 5.0, 6.0],
+        )
+        histogram2.record(4, {"label": "value"})
+        histogram2.record(5, {"label": "value"})
+        histogram2.record(6, {"label": "value"})
+
+        metrics = reader.get_metrics_data()
+        self.assertEqual(len(metrics.resource_metrics), 1)
+        self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1)
+        self.assertEqual(
+            len(metrics.resource_metrics[0].scope_metrics[0].metrics), 2
+        )
+        metric1 = metrics.resource_metrics[0].scope_metrics[0].metrics[0]
+        self.assertEqual(metric1.name, "testhistogram1")
+        self.assertEqual(
+            metric1.data.data_points[0].explicit_bounds, (1.0, 2.0, 3.0)
+        )
+        metric2 = metrics.resource_metrics[0].scope_metrics[0].metrics[1]
+        self.assertEqual(metric2.name, "testhistogram2")
+        self.assertEqual(
+            metric2.data.data_points[0].explicit_bounds, (4.0, 5.0, 6.0)
+        )
+
+    def test_explicit_aggregation_default_boundaries(self):
+        reader = InMemoryMetricReader(
+            preferred_aggregation={
+                Histogram: ExplicitBucketHistogramAggregation()
+            }
+        )
+        meter_provider = MeterProvider(
+            metric_readers=[reader],
+        )
+        meter = meter_provider.get_meter("testmeter")
+
+        histogram = meter.create_histogram(
+            "testhistogram",
+        )
+        histogram.record(1, {"label": "value"})
+        histogram.record(2, {"label": "value"})
+        histogram.record(3, {"label": "value"})
+
+        metrics = reader.get_metrics_data()
+        self.assertEqual(len(metrics.resource_metrics), 1)
+        self.assertEqual(len(metrics.resource_metrics[0].scope_metrics), 1)
+        self.assertEqual(
+            len(metrics.resource_metrics[0].scope_metrics[0].metrics), 1
+        )
+        metric = metrics.resource_metrics[0].scope_metrics[0].metrics[0]
+        self.assertEqual(metric.name, "testhistogram")
+        self.assertEqual(
+            metric.data.data_points[0].explicit_bounds,
+            _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES,
+        )


Reviews:
user: login: xrmx
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T13:27:53Z
user: login: emdneto
body: Right. Thank you for the fix.

That change fixes the bug while keeping the _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES in case boundaries are not set. I know we have this [test](https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-sdk/tests/metrics/test_aggregation.py#L450), but can you write one as part of the integration test? Like: asserting if we don't set the explicit buckets advisory it will keep the default ones?
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T14:44:44Z
user: login: emdneto
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T14:46:11Z
user: login: xrmx
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T07:04:23Z

Review comments:
diff_hunk: @@ -1387,18 +1387,17 @@ def _create_aggregation(
                 AggregationTemporality.CUMULATIVE
             )
 
-        if self._boundaries is None:
-            self._boundaries = (
-                instrument._advisory.explicit_bucket_boundaries
-                or _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES
-            )
+        if self._boundaries:
path: opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggregation.py
user: login: xrmx
body: ```suggestion
        if self._boundaries is not None:
```

Not sure passing empty boundaries is correct but I think this should check the default None
created_at: 2025-04-02T13:27:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   ([#4494](https://github.com/open-telemetry/opentelemetry-python/pull/4494))
 - Improve CI by cancelling stale runs and setting timeouts
   ([#4498](https://github.com/open-telemetry/opentelemetry-python/pull/4498))
+- Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521))
path: CHANGELOG.md
user: login: emdneto
body: ```suggestion
- Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories
 ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521))
```
created_at: 2025-04-02T14:46:11Z
author_association: MEMBER

PR comments:
user: login: linux-foundation-easycla[bot]
created_at: 2025-04-01T18:07:41Z
body: <a href="https://easycla.lfx.linuxfoundation.org/#/?version=2"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-signed.svg" alt="CLA Signed" align="left" height="28" width="328" ></a><br/><br />The committers listed above are authorized under a signed CLA.<ul><li>:white_check_mark: login: lseguy / name: Louis Séguy  (848865e0d65e9b4c6c43c6199c86feaee8d7af00, cd86bebd2235f2062753cb5529d599ca2c38fa4d, 6f42342a689433b456bf0df9828a76188c9e06fc, f0bb399e4927c3aee896b2163c35c5d5ed0d2de4, 29c3a0365a58b4603a8ce8b0811dce2b19c9f795, 54fa322a602602dd4d9c460add48d225db84f743)</li><li>:white_check_mark: login: emdneto / name: Emídio Neto  (fe6491c0c751d6cba5f6c43ba2ef6c1c72e87dbf)</li></ul><!-- Date Modified: 2025-04-02 20:05:54.658081 -->
author_association: NONE
user: login: xrmx
created_at: 2025-04-02T12:31:15Z
body: @lseguy any reason this is in draft?
author_association: CONTRIBUTOR
user: login: lseguy
created_at: 2025-04-02T13:21:07Z
body: > @lseguy any reason this is in draft?

I just had to sign the CLA and update the CHANGELOG.
I'd be happy to get your review if you don't mind, since you worked on #4361 and #4434.
author_association: CONTRIBUTOR
user: login: lseguy
created_at: 2025-04-02T17:10:18Z
body: > That change fixes the bug while keeping the _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES in case boundaries are not set. I know we have this [test](https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-sdk/tests/metrics/test_aggregation.py#L450), but can you write one as part of the integration test? Like: asserting if we don't set the explicit buckets advisory it will keep the default ones?

Added a new test case checking for default boundaries, let me know if that's not what you had in mind.
author_association: CONTRIBUTOR
user: login: emdneto
created_at: 2025-04-02T17:55:18Z
body: > > That change fixes the bug while keeping the _DEFAULT_EXPLICIT_BUCKET_HISTOGRAM_AGGREGATION_BOUNDARIES in case boundaries are not set. I know we have this [test](https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-sdk/tests/metrics/test_aggregation.py#L450), but can you write one as part of the integration test? Like: asserting if we don't set the explicit buckets advisory it will keep the default ones?
> 
> Added a new test case checking for default boundaries, let me know if that's not what you had in mind.

All good. Thank you. 

author_association: MEMBER