From 3157c1baa43d3aee36b3a6b2626f9fe69035b142 Mon Sep 17 00:00:00 2001 From: imamWahyudiz Date: Sat, 7 Nov 2026 02:33:41 +0700 Subject: [PATCH 1/3] fix(metrics): add explicit bucket boundaries advisory to duration histograms Fixes #5892 --- .changelog/5892.fixed | 1 + .../tests/test_exporter_metrics.py | 7 +++++ .../metrics/test_in_memory_metric_reader.py | 17 +++++++++++- .../test_periodic_exporting_metric_reader.py | 1 + .../_incubating/metrics/otel_metrics.py | 2 ++ .../tests/test_semconv.py | 26 +++++++++++++++++++ 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changelog/5892.fixed diff --git a/.changelog/5892.fixed b/.changelog/5892.fixed new file mode 100644 index 00000000000..8095efb304d --- /dev/null +++ b/.changelog/5892.fixed @@ -0,0 +1 @@ +`opentelemetry-sdk`: add empty explicit bucket boundaries advisory to duration histograms diff --git a/exporter/opentelemetry-exporter-otlp-common/tests/test_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-common/tests/test_exporter_metrics.py index f4830e54e81..e77883ef178 100644 --- a/exporter/opentelemetry-exporter-otlp-common/tests/test_exporter_metrics.py +++ b/exporter/opentelemetry-exporter-otlp-common/tests/test_exporter_metrics.py @@ -51,6 +51,13 @@ def test_factory_returns_exporter_metrics_when_enabled(self): self.assertIsInstance(metrics, ExporterMetrics) meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk") + meter = meter_provider.get_meter.return_value + meter.create_histogram.assert_called_once_with( + name="otel.sdk.exporter.operation.duration", + description="The duration of exporting a batch of telemetry records.", + unit="s", + explicit_bucket_boundaries_advisory=[], + ) def test_noop_export_operation_yields_result(self): metrics = NoOpExporterMetrics() diff --git a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py index 47aea81a424..aaedcba7793 100644 --- a/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_in_memory_metric_reader.py @@ -5,7 +5,7 @@ from time import sleep from unittest import TestCase -from unittest.mock import Mock +from unittest.mock import Mock, patch from opentelemetry.metrics import Observation from opentelemetry.sdk.metrics import Counter, MeterProvider @@ -166,6 +166,21 @@ def test_synchronous_gauge_cumulative_multiple_collect(self): [({"key": "value"}, 5)], ) + @patch.dict("os.environ", {"OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED": "true"}) + def test_metric_reader_metrics(self): + reader = InMemoryMetricReader() + meter_provider = Mock() + meter = Mock() + meter_provider.get_meter.return_value = meter + reader._set_meter_provider(meter_provider) + meter_provider.get_meter.assert_called_once_with("opentelemetry-sdk") + meter.create_histogram.assert_called_once_with( + name="otel.sdk.metric_reader.collection.duration", + description="The duration of the collect operation of the metric reader.", + unit="s", + explicit_bucket_boundaries_advisory=[], + ) + class TestInMemoryMetricReaderCardinalityLimits(TestCase): def test_default_limit(self): diff --git a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py index 3157c6dc7e4..5bbc7e3ff16 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -405,6 +405,7 @@ def test_metric_reader_metrics(self): point = metric.data.data_points[0] histogram = cast(HistogramDataPoint, point) self.assertEqual(histogram.count, 1) + self.assertEqual(histogram.explicit_bounds, ()) attrs = histogram.attributes assert attrs is not None self.assertEqual(attrs["otel.component.type"], "periodic_metric_reader") diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py index 4c80916c0e5..28b4e1ac53f 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py @@ -110,6 +110,7 @@ def create_otel_sdk_exporter_operation_duration(meter: Meter) -> Histogram: name=OTEL_SDK_EXPORTER_OPERATION_DURATION, description="The duration of exporting a batch of telemetry records.", unit="s", + explicit_bucket_boundaries_advisory=[], ) @@ -223,6 +224,7 @@ def create_otel_sdk_metric_reader_collection_duration(meter: Meter) -> Histogram name=OTEL_SDK_METRIC_READER_COLLECTION_DURATION, description="The duration of the collect operation of the metric reader.", unit="s", + explicit_bucket_boundaries_advisory=[], ) diff --git a/opentelemetry-semantic-conventions/tests/test_semconv.py b/opentelemetry-semantic-conventions/tests/test_semconv.py index 1594d61e47c..d561616a871 100644 --- a/opentelemetry-semantic-conventions/tests/test_semconv.py +++ b/opentelemetry-semantic-conventions/tests/test_semconv.py @@ -4,9 +4,35 @@ from importlib.util import find_spec from unittest import TestCase +from unittest.mock import Mock + +from opentelemetry.semconv._incubating.metrics.otel_metrics import ( + create_otel_sdk_exporter_operation_duration, + create_otel_sdk_metric_reader_collection_duration, +) class TestSemanticConventions(TestCase): def test_semantic_conventions(self): if find_spec("opentelemetry.semconv") is None: self.fail("opentelemetry-semantic-conventions not installed") + + def test_create_otel_sdk_exporter_operation_duration(self): + meter = Mock() + create_otel_sdk_exporter_operation_duration(meter) + meter.create_histogram.assert_called_once_with( + name="otel.sdk.exporter.operation.duration", + description="The duration of exporting a batch of telemetry records.", + unit="s", + explicit_bucket_boundaries_advisory=[], + ) + + def test_create_otel_sdk_metric_reader_collection_duration(self): + meter = Mock() + create_otel_sdk_metric_reader_collection_duration(meter) + meter.create_histogram.assert_called_once_with( + name="otel.sdk.metric_reader.collection.duration", + description="The duration of the collect operation of the metric reader.", + unit="s", + explicit_bucket_boundaries_advisory=[], + ) From 85f3bacd4fbcf60233ddf38c02cd90b935fa6e7e Mon Sep 17 00:00:00 2001 From: imamWahyudiz Date: Sat, 7 Nov 2026 13:18:49 +0700 Subject: [PATCH 2/3] refactor(metrics): call meter.create_histogram directly with explicit bucket advisory and match PR 5895 --- .changelog/{5892.fixed => 5895.fixed} | 0 .../exporter/otlp/common/_exporter_metrics.py | 9 +++++-- .../export/_metric_reader_metrics.py | 9 +++++-- .../_incubating/metrics/otel_metrics.py | 2 -- .../tests/test_semconv.py | 26 ------------------- 5 files changed, 14 insertions(+), 32 deletions(-) rename .changelog/{5892.fixed => 5895.fixed} (100%) diff --git a/.changelog/5892.fixed b/.changelog/5895.fixed similarity index 100% rename from .changelog/5892.fixed rename to .changelog/5895.fixed diff --git a/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/_exporter_metrics.py b/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/_exporter_metrics.py index 8d51674ef47..28bbc16a8a4 100644 --- a/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/_exporter_metrics.py +++ b/exporter/opentelemetry-exporter-otlp-common/src/opentelemetry/exporter/otlp/common/_exporter_metrics.py @@ -18,11 +18,11 @@ OtelComponentTypeValues, ) from opentelemetry.semconv._incubating.metrics.otel_metrics import ( + OTEL_SDK_EXPORTER_OPERATION_DURATION, create_otel_sdk_exporter_log_exported, create_otel_sdk_exporter_log_inflight, create_otel_sdk_exporter_metric_data_point_exported, create_otel_sdk_exporter_metric_data_point_inflight, - create_otel_sdk_exporter_operation_duration, create_otel_sdk_exporter_span_exported, create_otel_sdk_exporter_span_inflight, ) @@ -110,7 +110,12 @@ def __init__( meter = meter_provider.get_meter("opentelemetry-sdk") self._inflight = create_inflight(meter) self._exported = create_exported(meter) - self._duration = create_otel_sdk_exporter_operation_duration(meter) + self._duration = meter.create_histogram( + name=OTEL_SDK_EXPORTER_OPERATION_DURATION, + description="The duration of exporting a batch of telemetry records.", + unit="s", + explicit_bucket_boundaries_advisory=[], + ) @contextmanager def export_operation(self, num_items: int) -> Iterator[ExportResult]: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py index 019f1cc1725..ad465eccb9a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py @@ -10,7 +10,7 @@ OTEL_COMPONENT_TYPE, ) from opentelemetry.semconv._incubating.metrics.otel_metrics import ( - create_otel_sdk_metric_reader_collection_duration, + OTEL_SDK_METRIC_READER_COLLECTION_DURATION, ) _component_counter = Counter() @@ -37,7 +37,12 @@ def __init__(self, component_type: str, meter_provider: MeterProvider) -> None: OTEL_COMPONENT_NAME: f"{component_type}/{count}", } - self._collection_duration = create_otel_sdk_metric_reader_collection_duration(meter) + self._collection_duration = meter.create_histogram( + name=OTEL_SDK_METRIC_READER_COLLECTION_DURATION, + description="The duration of the collect operation of the metric reader.", + unit="s", + explicit_bucket_boundaries_advisory=[], + ) def record_collection(self, duration: float) -> None: self._collection_duration.record(duration, self._standard_attrs) diff --git a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py index 28b4e1ac53f..4c80916c0e5 100644 --- a/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py +++ b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/otel_metrics.py @@ -110,7 +110,6 @@ def create_otel_sdk_exporter_operation_duration(meter: Meter) -> Histogram: name=OTEL_SDK_EXPORTER_OPERATION_DURATION, description="The duration of exporting a batch of telemetry records.", unit="s", - explicit_bucket_boundaries_advisory=[], ) @@ -224,7 +223,6 @@ def create_otel_sdk_metric_reader_collection_duration(meter: Meter) -> Histogram name=OTEL_SDK_METRIC_READER_COLLECTION_DURATION, description="The duration of the collect operation of the metric reader.", unit="s", - explicit_bucket_boundaries_advisory=[], ) diff --git a/opentelemetry-semantic-conventions/tests/test_semconv.py b/opentelemetry-semantic-conventions/tests/test_semconv.py index d561616a871..1594d61e47c 100644 --- a/opentelemetry-semantic-conventions/tests/test_semconv.py +++ b/opentelemetry-semantic-conventions/tests/test_semconv.py @@ -4,35 +4,9 @@ from importlib.util import find_spec from unittest import TestCase -from unittest.mock import Mock - -from opentelemetry.semconv._incubating.metrics.otel_metrics import ( - create_otel_sdk_exporter_operation_duration, - create_otel_sdk_metric_reader_collection_duration, -) class TestSemanticConventions(TestCase): def test_semantic_conventions(self): if find_spec("opentelemetry.semconv") is None: self.fail("opentelemetry-semantic-conventions not installed") - - def test_create_otel_sdk_exporter_operation_duration(self): - meter = Mock() - create_otel_sdk_exporter_operation_duration(meter) - meter.create_histogram.assert_called_once_with( - name="otel.sdk.exporter.operation.duration", - description="The duration of exporting a batch of telemetry records.", - unit="s", - explicit_bucket_boundaries_advisory=[], - ) - - def test_create_otel_sdk_metric_reader_collection_duration(self): - meter = Mock() - create_otel_sdk_metric_reader_collection_duration(meter) - meter.create_histogram.assert_called_once_with( - name="otel.sdk.metric_reader.collection.duration", - description="The duration of the collect operation of the metric reader.", - unit="s", - explicit_bucket_boundaries_advisory=[], - ) From 1ef1b865f16c1ff139a4c3e03082e4d71aa5c8ff Mon Sep 17 00:00:00 2001 From: imamWahyudiz Date: Sat, 7 Nov 2026 20:17:53 +0700 Subject: [PATCH 3/3] test: assert empty bounds for exporter duration in test_proto_span_exporter --- .../tests/test_proto_span_exporter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py index b0dbb47a7cc..745eff3a1f9 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_span_exporter.py @@ -683,6 +683,7 @@ def test_retry_timeout_records_metrics(self): metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 3) self.assertEqual(metrics[0].name, "otel.sdk.exporter.operation.duration") + self.assertEqual(metrics[0].data.data_points[0].explicit_bounds, ()) self.assert_standard_metric_attrs(metrics[0].data.data_points[0].attributes) self.assertEqual( metrics[0].data.data_points[0].attributes["http.response.status_code"], @@ -716,6 +717,7 @@ def test_export_connection_error_records_metrics(self): scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(metrics[0].name, "otel.sdk.exporter.operation.duration") + self.assertEqual(metrics[0].data.data_points[0].explicit_bounds, ()) self.assertIn("error.type", metrics[0].data.data_points[0].attributes) self.assertNotIn( "http.response.status_code",