From 62f385fc7c3eaa6c0e5eeadab0ef56043ccd7ea6 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 27 Feb 2026 15:49:31 +0900 Subject: [PATCH 1/8] feat(metrics): implement metric reader metrics --- .../sdk/_logs/_internal/export/__init__.py | 6 +-- .../sdk/metrics/_internal/__init__.py | 3 ++ .../sdk/metrics/_internal/export/__init__.py | 27 ++++++++++++-- .../export/_metric_reader_metrics.py | 36 ++++++++++++++++++ .../sdk/trace/export/__init__.py | 6 +-- .../test_periodic_exporting_metric_reader.py | 37 +++++++++++++++---- 6 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index f12b9dd8a2d..ddae0de7ddf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -116,9 +116,9 @@ class ConsoleLogRecordExporter(LogRecordExporter): def __init__( self, out: IO = sys.stdout, - formatter: Callable[ - [ReadableLogRecord], str - ] = lambda record: record.to_json() + linesep, + formatter: Callable[[ReadableLogRecord], str] = lambda record: ( + record.to_json() + linesep + ), ): self.out = out self.formatter = formatter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index 1ffcd5a14c5..f5013bea074 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -38,6 +38,7 @@ OTEL_METRICS_EXEMPLAR_FILTER, OTEL_SDK_DISABLED, ) +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.exemplar import ( AlwaysOffExemplarFilter, @@ -458,6 +459,8 @@ def __init__( metric_reader._set_collect_callback( self._measurement_consumer.collect ) + if isinstance(metric_reader, PeriodicExportingMetricReader): + metric_reader._set_meter_provider(self) def force_flush(self, timeout_millis: float = 10_000) -> bool: deadline_ns = time_ns() + timeout_millis * 10**6 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index cdbad3e3432..475cc7cff9b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -22,13 +22,14 @@ from os import environ, linesep from sys import stdout from threading import Event, Lock, RLock, Thread -from time import time_ns +from time import monotonic, time_ns from typing import IO, Callable, Iterable, Optional from typing_extensions import final # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal +from opentelemetry.metrics import MeterProvider, NoOpMeterProvider from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, @@ -61,8 +62,13 @@ _UpDownCounter, ) from opentelemetry.sdk.metrics._internal.point import MetricsData +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) from opentelemetry.util._once import Once +from ._metric_reader_metrics import MetricReaderMetrics + _logger = getLogger(__name__) @@ -144,9 +150,9 @@ class ConsoleMetricExporter(MetricExporter): def __init__( self, out: IO = stdout, - formatter: Callable[ - [MetricsData], str - ] = lambda metrics_data: metrics_data.to_json() + linesep, + formatter: Callable[[MetricsData], str] = lambda metrics_data: ( + metrics_data.to_json() + linesep + ), preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[ @@ -506,6 +512,9 @@ def __init__( f"interval value {self._export_interval_millis} is invalid \ and needs to be larger than zero." ) + self._metrics = MetricReaderMetrics( + OtelComponentTypeValues.PERIODIC_METRIC_READER, NoOpMeterProvider() + ) def _at_fork_reinit(self): self._daemon_thread = Thread( @@ -518,6 +527,7 @@ def _at_fork_reinit(self): def _ticker(self) -> None: interval_secs = self._export_interval_millis / 1e3 while not self._shutdown_event.wait(interval_secs): + start_time = monotonic() try: self.collect(timeout_millis=self._export_timeout_millis) except MetricsTimeoutError: @@ -526,6 +536,10 @@ def _ticker(self) -> None: interval_secs, exc_info=True, ) + finally: + duration = monotonic() - start_time + self._metrics.record_collection(duration) + # one last collection below before shutting down completely try: self.collect(timeout_millis=self._export_interval_millis) @@ -552,6 +566,11 @@ def _receive_metrics( _logger.exception("Exception while exporting metrics") detach(token) + def _set_meter_provider(self, meter_provider: MeterProvider) -> None: + self._metrics = MetricReaderMetrics( + OtelComponentTypeValues.PERIODIC_METRIC_READER, meter_provider + ) + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: deadline_ns = time_ns() + timeout_millis * 10**6 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 new file mode 100644 index 00000000000..bb3d8dc3df9 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/_metric_reader_metrics.py @@ -0,0 +1,36 @@ +from collections import Counter + + +from opentelemetry.metrics import MeterProvider +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OTEL_COMPONENT_NAME, + OTEL_COMPONENT_TYPE, +) +from opentelemetry.semconv._incubating.metrics.otel_metrics import ( + create_otel_sdk_metric_reader_collection_duration, +) + + +_component_counter = Counter() + + +class MetricReaderMetrics: + def __init__( + self, component_type: str, meter_provider: MeterProvider + ) -> None: + meter = meter_provider.get_meter("opentelemetry-sdk") + + count = _component_counter[component_type] + _component_counter[component_type] = count + 1 + + self._standard_attrs = { + OTEL_COMPONENT_TYPE: component_type, + OTEL_COMPONENT_NAME: f"{component_type}/{count}", + } + + self._collection_duration = ( + create_otel_sdk_metric_reader_collection_duration(meter) + ) + + def record_collection(self, duration: float) -> None: + self._collection_duration.record(duration, self._standard_attrs) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index a9108b7337a..d853dfd6c40 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -300,9 +300,9 @@ def __init__( self, service_name: str | None = None, out: typing.IO = sys.stdout, - formatter: typing.Callable[ - [ReadableSpan], str - ] = lambda span: span.to_json() + linesep, + formatter: typing.Callable[[ReadableSpan], str] = lambda span: ( + span.to_json() + linesep + ), ): self.out = out self.formatter = formatter 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 8722effe385..ca15a43c0c9 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -24,8 +24,9 @@ import pytest -from opentelemetry.sdk.metrics import Counter, MetricsTimeoutError +from opentelemetry.sdk.metrics import Counter, MetricsTimeoutError, MeterProvider from opentelemetry.sdk.metrics._internal import _Counter +from opentelemetry.sdk.metrics._internal.point import MetricsData from opentelemetry.sdk.metrics.export import ( AggregationTemporality, Gauge, @@ -48,7 +49,7 @@ def __init__( self, wait=0, preferred_temporality=None, preferred_aggregation=None ): self.wait = wait - self.metrics = [] + self.metrics: list[MetricsData] = [] self._shutdown = False super().__init__( preferred_temporality=preferred_temporality, @@ -57,13 +58,13 @@ def __init__( def export( self, - metrics_data: Sequence[Metric], + metrics_data: MetricsData, timeout_millis: float = 10_000, **kwargs, ) -> MetricExportResult: sleep(self.wait) - self.metrics.extend(metrics_data) - return True + self.metrics.append(metrics_data) + return MetricExportResult.SUCCESS def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: self._shutdown = True @@ -137,7 +138,7 @@ def test_defaults(self): pmr.shutdown() def _create_periodic_reader( - self, metrics, exporter, collect_wait=0, interval=60000, timeout=30000 + self, metrics: list[Metric], exporter, collect_wait=0, interval=60000, timeout=30000 ): pmr = PeriodicExportingMetricReader( exporter, @@ -146,8 +147,7 @@ def _create_periodic_reader( ) def _collect(reader, timeout_millis): - sleep(collect_wait) - pmr._receive_metrics(metrics, timeout_millis) + return metrics pmr._set_collect_callback(_collect) return pmr @@ -280,3 +280,24 @@ def test_metric_exporer_gc(self): weak_ref(), "The PeriodicExportingMetricReader object created by this test wasn't garbage collected", ) + + def test_metric_reader_metrics(self): + exporter = FakeMetricsExporter() + pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=1) + mp = MeterProvider(metric_readers=[pmr]) + + counter = mp.get_meter("test").create_counter("test_counter") + counter.add(1) + + sleep(0.1) + self.assertEqual(len(exporter.metrics), 1) + # Need a second collection to get the metric we recorded during first collection + exporter.metrics.clear() + sleep(0.1) + self.assertEqual(len(exporter.metrics), 2) + metric_data = exporter.metrics[1] + self.assertEqual( + metric_data.resource_metrics[0].scope_metrics[0].metrics[0].name, + "otel.sdk.metric_reader.collection.duration", + ) + mp.shutdown() From 563f062404e9a90dfe9610449db08a936207c900 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 12 Mar 2026 14:04:46 +0900 Subject: [PATCH 2/8] Finish --- .../exporter/prometheus/__init__.py | 6 ++- .../sdk/metrics/_internal/__init__.py | 3 +- .../sdk/metrics/_internal/export/__init__.py | 38 ++++++++------ ...t_explicit_bucket_histogram_aggregation.py | 4 ++ .../test_exponential_bucket_histogram.py | 7 +++ .../test_periodic_exporting_metric_reader.py | 49 +++++++++++++++---- 6 files changed, 78 insertions(+), 29 deletions(-) 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 fa89da4e71e..abf9d7f163f 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -105,6 +105,9 @@ MetricsData, Sum, ) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) from opentelemetry.util.types import Attributes _logger = getLogger(__name__) @@ -140,7 +143,8 @@ def __init__(self, disable_target_info: bool = False) -> None: ObservableCounter: AggregationTemporality.CUMULATIVE, ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, - } + }, + otel_component_type=OtelComponentTypeValues.PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER, ) self._collector = _CustomCollector(disable_target_info) REGISTRY.register(self._collector) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index f5013bea074..d7f0b581285 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -459,8 +459,7 @@ def __init__( metric_reader._set_collect_callback( self._measurement_consumer.collect ) - if isinstance(metric_reader, PeriodicExportingMetricReader): - metric_reader._set_meter_provider(self) + metric_reader._set_meter_provider(self) def force_flush(self, timeout_millis: float = 10_000) -> bool: deadline_ns = time_ns() + timeout_millis * 10**6 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index cc6ce590615..d395270461d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -22,20 +22,20 @@ from os import environ, linesep from sys import stdout from threading import Event, Lock, RLock, Thread -from time import monotonic, time_ns +from time import perf_counter, time_ns from typing import IO, Callable, Iterable, Optional from typing_extensions import final # This kind of import is needed to avoid Sphinx errors. import opentelemetry.sdk.metrics._internal -from opentelemetry.metrics import MeterProvider, NoOpMeterProvider from opentelemetry.context import ( _SUPPRESS_INSTRUMENTATION_KEY, attach, detach, set_value, ) +from opentelemetry.metrics import MeterProvider, NoOpMeterProvider from opentelemetry.sdk.environment_variables import ( OTEL_METRIC_EXPORT_INTERVAL, OTEL_METRIC_EXPORT_TIMEOUT, @@ -226,6 +226,8 @@ def __init__( type, "opentelemetry.sdk.metrics.view.Aggregation" ] | None = None, + *, + otel_component_type: str | None = None, ) -> None: self._collect: Callable[ [ @@ -324,6 +326,13 @@ def __init__( else: raise Exception(f"Invalid instrument class found {typ}") + self._otel_component_type = ( + otel_component_type or type(self).__qualname__ + ) + self._metrics = MetricReaderMetrics( + self._otel_component_type, NoOpMeterProvider() + ) + @final def collect(self, timeout_millis: float = 10_000) -> None: """Collects the metrics from the internal SDK state and @@ -343,7 +352,11 @@ def collect(self, timeout_millis: float = 10_000) -> None: ) return - metrics = self._collect(self, timeout_millis=timeout_millis) + start_time = perf_counter() + try: + metrics = self._collect(self, timeout_millis=timeout_millis) + finally: + self._metrics.record_collection(perf_counter() - start_time) if metrics is not None: self._receive_metrics( @@ -374,6 +387,11 @@ def _receive_metrics( ) -> None: """Called by `MetricReader.collect` when it receives a batch of metrics""" + def _set_meter_provider(self, meter_provider: MeterProvider) -> None: + self._metrics = MetricReaderMetrics( + self._otel_component_type, meter_provider + ) + def force_flush(self, timeout_millis: float = 10_000) -> bool: self.collect(timeout_millis=timeout_millis) return True @@ -457,6 +475,7 @@ def __init__( super().__init__( preferred_temporality=exporter._preferred_temporality, preferred_aggregation=exporter._preferred_aggregation, + otel_component_type=OtelComponentTypeValues.PERIODIC_METRIC_READER.value, ) # This lock is held whenever calling self._exporter.export() to prevent concurrent @@ -512,9 +531,6 @@ def __init__( f"interval value {self._export_interval_millis} is invalid \ and needs to be larger than zero." ) - self._metrics = MetricReaderMetrics( - OtelComponentTypeValues.PERIODIC_METRIC_READER, NoOpMeterProvider() - ) def _at_fork_reinit(self): self._daemon_thread = Thread( @@ -527,7 +543,6 @@ def _at_fork_reinit(self): def _ticker(self) -> None: interval_secs = self._export_interval_millis / 1e3 while not self._shutdown_event.wait(interval_secs): - start_time = monotonic() try: self.collect(timeout_millis=self._export_timeout_millis) except MetricsTimeoutError: @@ -536,10 +551,6 @@ def _ticker(self) -> None: interval_secs, exc_info=True, ) - finally: - duration = monotonic() - start_time - self._metrics.record_collection(duration) - # one last collection below before shutting down completely try: self.collect(timeout_millis=self._export_interval_millis) @@ -566,11 +577,6 @@ def _receive_metrics( _logger.exception("Exception while exporting metrics") detach(token) - def _set_meter_provider(self, meter_provider: MeterProvider) -> None: - self._metrics = MetricReaderMetrics( - OtelComponentTypeValues.PERIODIC_METRIC_READER, meter_provider - ) - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: deadline_ns = time_ns() + timeout_millis * 10**6 diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py b/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py index 05ccd1469c9..c66c32a27b0 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py @@ -18,6 +18,7 @@ from pytest import mark +from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk.metrics import Histogram, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -46,6 +47,9 @@ def test_synchronous_delta_temporality(self): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_exponential_bucket_histogram.py b/opentelemetry-sdk/tests/metrics/integration_test/test_exponential_bucket_histogram.py index fa44cc6ce50..2e066e102da 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_exponential_bucket_histogram.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_exponential_bucket_histogram.py @@ -18,6 +18,7 @@ from pytest import mark +from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk.metrics import Histogram, MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -57,6 +58,9 @@ def test_synchronous_delta_temporality(self): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") @@ -191,6 +195,9 @@ def test_synchronous_cumulative_temporality(self): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") 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 a3a2f3912a1..3e47e577689 100644 --- a/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py +++ b/opentelemetry-sdk/tests/metrics/test_periodic_exporting_metric_reader.py @@ -19,15 +19,19 @@ import weakref from logging import WARNING from time import sleep, time_ns -from typing import Optional +from typing import Optional, cast from unittest.mock import Mock import pytest -from opentelemetry.sdk.metrics import Counter, MetricsTimeoutError, MeterProvider +from opentelemetry.sdk.metrics import ( + Counter, + MeterProvider, + MetricsTimeoutError, +) from opentelemetry.sdk.metrics._internal import _Counter -from opentelemetry.sdk.metrics._internal.point import MetricsData from opentelemetry.sdk.metrics._internal.point import ( + HistogramDataPoint, MetricsData, ResourceMetrics, ScopeMetrics, @@ -313,21 +317,46 @@ def test_metric_exporer_gc(self): def test_metric_reader_metrics(self): exporter = FakeMetricsExporter() - pmr = PeriodicExportingMetricReader(exporter, export_interval_millis=1) + pmr = PeriodicExportingMetricReader( + exporter, export_interval_millis=100000 + ) mp = MeterProvider(metric_readers=[pmr]) counter = mp.get_meter("test").create_counter("test_counter") counter.add(1) - sleep(0.1) + mp.force_flush() self.assertEqual(len(exporter.metrics), 1) # Need a second collection to get the metric we recorded during first collection exporter.metrics.clear() - sleep(0.1) - self.assertEqual(len(exporter.metrics), 2) - metric_data = exporter.metrics[1] + mp.force_flush() + self.assertEqual(len(exporter.metrics), 1) + metric_data = exporter.metrics[0] + + scope_metrics = [ + sm + for sm in metric_data.resource_metrics[0].scope_metrics + if sm.scope.name == "opentelemetry-sdk" + ] + self.assertEqual(len(scope_metrics), 1) + reader_metrics = [ + m + for m in scope_metrics[0].metrics + if m.name == "otel.sdk.metric_reader.collection.duration" + ] + self.assertEqual(len(reader_metrics), 1) + metric = reader_metrics[0] + + point = metric.data.data_points[0] + histogram = cast(HistogramDataPoint, point) + self.assertEqual(histogram.count, 1) + attrs = histogram.attributes + assert attrs is not None self.assertEqual( - metric_data.resource_metrics[0].scope_metrics[0].metrics[0].name, - "otel.sdk.metric_reader.collection.duration", + attrs["otel.component.type"], "periodic_metric_reader" ) + name = attrs["otel.component.name"] + assert isinstance(name, str) + self.assertTrue(name.startswith("periodic_metric_reader/")) + mp.shutdown() From cf2a8ba1d76204f1e815d576003e28a7e33f4a45 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 12 Mar 2026 14:08:20 +0900 Subject: [PATCH 3/8] Changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9130b344751..8743186f709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4958](https://github.com/open-telemetry/opentelemetry-python/pull/4958)) - `opentelemetry-sdk`: fix type annotations on `MetricReader` and related types ([#4938](https://github.com/open-telemetry/opentelemetry-python/pull/4938/)) -- Implement log creation metric +- `opentelemetry-sdk`: implement log creation metric ([#4935](https://github.com/open-telemetry/opentelemetry-python/pull/4935)) +- `opentelemetry-sdk`: implement metric reader metrics + ([#4970](https://github.com/open-telemetry/opentelemetry-python/pull/4970)) - `opentelemetry-sdk`: upgrade vendored OTel configuration schema from v1.0.0-rc.3 to v1.0.0 ([#4965](https://github.com/open-telemetry/opentelemetry-python/pull/4965)) From e136d9c7d3045b5ba7012aa4585f3cbe5c492352 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 12 Mar 2026 14:11:04 +0900 Subject: [PATCH 4/8] Fix prometheus test --- .../src/opentelemetry/exporter/prometheus/__init__.py | 2 +- .../tests/test_prometheus_exporter.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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 abf9d7f163f..7c7b366e19d 100644 --- a/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py +++ b/exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py @@ -144,7 +144,7 @@ def __init__(self, disable_target_info: bool = False) -> None: ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, ObservableGauge: AggregationTemporality.CUMULATIVE, }, - otel_component_type=OtelComponentTypeValues.PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER, + otel_component_type=OtelComponentTypeValues.PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER.value, ) self._collector = _CustomCollector(disable_target_info) REGISTRY.register(self._collector) diff --git a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py index d98c69cb860..15a92840090 100644 --- a/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py +++ b/exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py @@ -27,6 +27,7 @@ PrometheusMetricReader, _CustomCollector, ) +from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( AggregationTemporality, @@ -332,6 +333,8 @@ def test_check_value(self): def test_multiple_collection_calls(self): metric_reader = PrometheusMetricReader() provider = MeterProvider(metric_readers=[metric_reader]) + # Disable SDK metrics since they are not constant across collections + metric_reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("getting-started", "0.1.2") counter = meter.create_counter("counter") counter.add(1) From 46a65f0198b2ba1689fcf34c9c600300d0c5f61d Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 12 Mar 2026 14:20:05 +0900 Subject: [PATCH 5/8] Fix test --- .../integration_test/test_sum_aggregation.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py b/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py index b876ac99064..61371416508 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py @@ -21,7 +21,7 @@ from pytest import mark from opentelemetry.context import Context -from opentelemetry.metrics import Observation +from opentelemetry.metrics import NoOpMeterProvider, Observation from opentelemetry.sdk.metrics import Counter, MeterProvider, ObservableCounter from opentelemetry.sdk.metrics._internal.exemplar import AlwaysOnExemplarFilter from opentelemetry.sdk.metrics.export import ( @@ -33,7 +33,7 @@ class TestSumAggregation(TestCase): @mark.skipif( - system() != "Linux", + system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" @@ -68,6 +68,9 @@ def observable_counter_callback(callback_options): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") meter.create_observable_counter( @@ -156,7 +159,7 @@ def observable_counter_callback(callback_options): self.assertIsNone(metrics_data) @mark.skipif( - system() != "Linux", + system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" @@ -191,6 +194,9 @@ def observable_counter_callback(callback_options): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") meter.create_observable_counter( @@ -251,7 +257,7 @@ def observable_counter_callback(callback_options): self.assertIsNone(metrics_data) @mark.skipif( - system() != "Linux", + system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" @@ -267,6 +273,9 @@ def test_synchronous_delta_temporality(self): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") counter = meter.create_counter("counter") @@ -378,7 +387,7 @@ def test_synchronous_delta_temporality(self): provider.shutdown() @mark.skipif( - system() != "Linux", + system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" @@ -394,6 +403,9 @@ def test_synchronous_cumulative_temporality(self): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") counter = meter.create_counter("counter") From 7cfa827ed2bf2e677ddd71af9d8a163705e6aa39 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 12 Mar 2026 14:22:59 +0900 Subject: [PATCH 6/8] Fix test --- .../test_explicit_bucket_histogram_aggregation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py b/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py index c66c32a27b0..8aa2321cb47 100644 --- a/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py +++ b/opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py @@ -163,7 +163,7 @@ def test_synchronous_delta_temporality(self): provider.shutdown() @mark.skipif( - system() != "Linux", + system() == "Windows", reason=( "Tests fail because Windows time_ns resolution is too low so " "two different time measurements may end up having the exact same" @@ -181,6 +181,9 @@ def test_synchronous_cumulative_temporality(self): ) provider = MeterProvider(metric_readers=[reader]) + # Disable SDK metrics + # pylint: disable=protected-access + reader._set_meter_provider(NoOpMeterProvider()) meter = provider.get_meter("name", "version") histogram = meter.create_histogram("histogram") From 05f71648e5e5ddd4b55b6a805e25f2e414694f0d Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 12 Mar 2026 14:24:59 +0900 Subject: [PATCH 7/8] Format --- .../src/opentelemetry/sdk/metrics/_internal/__init__.py | 2 +- .../sdk/metrics/_internal/export/_metric_reader_metrics.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index d7f0b581285..d01272c8427 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -38,7 +38,6 @@ OTEL_METRICS_EXEMPLAR_FILTER, OTEL_SDK_DISABLED, ) -from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError from opentelemetry.sdk.metrics._internal.exemplar import ( AlwaysOffExemplarFilter, @@ -62,6 +61,7 @@ from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util._once import Once 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 bb3d8dc3df9..435d9c2da7b 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 @@ -1,6 +1,5 @@ from collections import Counter - from opentelemetry.metrics import MeterProvider from opentelemetry.semconv._incubating.attributes.otel_attributes import ( OTEL_COMPONENT_NAME, @@ -10,7 +9,6 @@ create_otel_sdk_metric_reader_collection_duration, ) - _component_counter = Counter() From ce30ee67420503439b65ff48636b4988c7561db9 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 12 Mar 2026 14:27:19 +0900 Subject: [PATCH 8/8] Format --- .../src/opentelemetry/sdk/metrics/_internal/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py index d01272c8427..89bef8edbb6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py @@ -61,7 +61,6 @@ from opentelemetry.sdk.metrics._internal.sdk_configuration import ( SdkConfiguration, ) -from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope from opentelemetry.util._once import Once