Skip to content

Commit 57821e9

Browse files
authored
feat(metrics): implement metric reader metrics (#4970)
1 parent 13373c3 commit 57821e9

10 files changed

Lines changed: 160 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
([#4958](https://github.com/open-telemetry/opentelemetry-python/pull/4958))
1919
- `opentelemetry-sdk`: fix type annotations on `MetricReader` and related types
2020
([#4938](https://github.com/open-telemetry/opentelemetry-python/pull/4938/))
21-
- Implement log creation metric
21+
- `opentelemetry-sdk`: implement log creation metric
2222
([#4935](https://github.com/open-telemetry/opentelemetry-python/pull/4935))
23+
- `opentelemetry-sdk`: implement metric reader metrics
24+
([#4970](https://github.com/open-telemetry/opentelemetry-python/pull/4970))
2325
- `opentelemetry-sdk`: upgrade vendored OTel configuration schema from v1.0.0-rc.3 to v1.0.0
2426
([#4965](https://github.com/open-telemetry/opentelemetry-python/pull/4965))
2527
- improve check-links ci job

exporter/opentelemetry-exporter-prometheus/src/opentelemetry/exporter/prometheus/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@
105105
MetricsData,
106106
Sum,
107107
)
108+
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
109+
OtelComponentTypeValues,
110+
)
108111
from opentelemetry.util.types import Attributes
109112

110113
_logger = getLogger(__name__)
@@ -142,7 +145,8 @@ def __init__(
142145
ObservableCounter: AggregationTemporality.CUMULATIVE,
143146
ObservableUpDownCounter: AggregationTemporality.CUMULATIVE,
144147
ObservableGauge: AggregationTemporality.CUMULATIVE,
145-
}
148+
},
149+
otel_component_type=OtelComponentTypeValues.PROMETHEUS_HTTP_TEXT_METRIC_EXPORTER,
146150
)
147151
self._collector = _CustomCollector(
148152
disable_target_info=disable_target_info, prefix=prefix

exporter/opentelemetry-exporter-prometheus/tests/test_prometheus_exporter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
PrometheusMetricReader,
2828
_CustomCollector,
2929
)
30+
from opentelemetry.metrics import NoOpMeterProvider
3031
from opentelemetry.sdk.metrics import MeterProvider
3132
from opentelemetry.sdk.metrics.export import (
3233
AggregationTemporality,
@@ -332,6 +333,8 @@ def test_check_value(self):
332333
def test_multiple_collection_calls(self):
333334
metric_reader = PrometheusMetricReader()
334335
provider = MeterProvider(metric_readers=[metric_reader])
336+
# Disable SDK metrics since they are not constant across collections
337+
metric_reader._set_meter_provider(NoOpMeterProvider())
335338
meter = provider.get_meter("getting-started", "0.1.2")
336339
counter = meter.create_counter("counter")
337340
counter.add(1)

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ def __init__(
470470
metric_reader._set_collect_callback(
471471
self._measurement_consumer.collect
472472
)
473+
metric_reader._set_meter_provider(self)
473474

474475
def force_flush(self, timeout_millis: float = 10_000) -> bool:
475476
deadline_ns = time_ns() + timeout_millis * 10**6

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from os import environ, linesep
2323
from sys import stdout
2424
from threading import Event, Lock, RLock, Thread
25-
from time import time_ns
25+
from time import perf_counter, time_ns
2626
from typing import IO, Callable, Iterable, Optional
2727

2828
from typing_extensions import final
@@ -35,6 +35,7 @@
3535
detach,
3636
set_value,
3737
)
38+
from opentelemetry.metrics import MeterProvider, NoOpMeterProvider
3839
from opentelemetry.sdk.environment_variables import (
3940
OTEL_METRIC_EXPORT_INTERVAL,
4041
OTEL_METRIC_EXPORT_TIMEOUT,
@@ -61,8 +62,13 @@
6162
_UpDownCounter,
6263
)
6364
from opentelemetry.sdk.metrics._internal.point import MetricsData
65+
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
66+
OtelComponentTypeValues,
67+
)
6468
from opentelemetry.util._once import Once
6569

70+
from ._metric_reader_metrics import MetricReaderMetrics
71+
6672
_logger = getLogger(__name__)
6773

6874

@@ -220,6 +226,8 @@ def __init__(
220226
type, "opentelemetry.sdk.metrics.view.Aggregation"
221227
]
222228
| None = None,
229+
*,
230+
otel_component_type: OtelComponentTypeValues | None = None,
223231
) -> None:
224232
self._collect: Callable[
225233
[
@@ -318,6 +326,15 @@ def __init__(
318326
else:
319327
raise Exception(f"Invalid instrument class found {typ}")
320328

329+
self._otel_component_type = (
330+
otel_component_type.value
331+
if otel_component_type
332+
else type(self).__qualname__
333+
)
334+
self._metrics = MetricReaderMetrics(
335+
self._otel_component_type, NoOpMeterProvider()
336+
)
337+
321338
@final
322339
def collect(self, timeout_millis: float = 10_000) -> None:
323340
"""Collects the metrics from the internal SDK state and
@@ -337,7 +354,11 @@ def collect(self, timeout_millis: float = 10_000) -> None:
337354
)
338355
return
339356

340-
metrics = self._collect(self, timeout_millis=timeout_millis)
357+
start_time = perf_counter()
358+
try:
359+
metrics = self._collect(self, timeout_millis=timeout_millis)
360+
finally:
361+
self._metrics.record_collection(perf_counter() - start_time)
341362

342363
if metrics is not None:
343364
self._receive_metrics(
@@ -368,6 +389,11 @@ def _receive_metrics(
368389
) -> None:
369390
"""Called by `MetricReader.collect` when it receives a batch of metrics"""
370391

392+
def _set_meter_provider(self, meter_provider: MeterProvider) -> None:
393+
self._metrics = MetricReaderMetrics(
394+
self._otel_component_type, meter_provider
395+
)
396+
371397
def force_flush(self, timeout_millis: float = 10_000) -> bool:
372398
self.collect(timeout_millis=timeout_millis)
373399
return True
@@ -451,6 +477,7 @@ def __init__(
451477
super().__init__(
452478
preferred_temporality=exporter._preferred_temporality,
453479
preferred_aggregation=exporter._preferred_aggregation,
480+
otel_component_type=OtelComponentTypeValues.PERIODIC_METRIC_READER,
454481
)
455482

456483
# This lock is held whenever calling self._exporter.export() to prevent concurrent
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from collections import Counter
2+
3+
from opentelemetry.metrics import MeterProvider
4+
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
5+
OTEL_COMPONENT_NAME,
6+
OTEL_COMPONENT_TYPE,
7+
)
8+
from opentelemetry.semconv._incubating.metrics.otel_metrics import (
9+
create_otel_sdk_metric_reader_collection_duration,
10+
)
11+
12+
_component_counter = Counter()
13+
14+
15+
class MetricReaderMetrics:
16+
def __init__(
17+
self, component_type: str, meter_provider: MeterProvider
18+
) -> None:
19+
meter = meter_provider.get_meter("opentelemetry-sdk")
20+
21+
count = _component_counter[component_type]
22+
_component_counter[component_type] = count + 1
23+
24+
self._standard_attrs = {
25+
OTEL_COMPONENT_TYPE: component_type,
26+
OTEL_COMPONENT_NAME: f"{component_type}/{count}",
27+
}
28+
29+
self._collection_duration = (
30+
create_otel_sdk_metric_reader_collection_duration(meter)
31+
)
32+
33+
def record_collection(self, duration: float) -> None:
34+
self._collection_duration.record(duration, self._standard_attrs)

opentelemetry-sdk/tests/metrics/integration_test/test_explicit_bucket_histogram_aggregation.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from pytest import mark
2020

21+
from opentelemetry.metrics import NoOpMeterProvider
2122
from opentelemetry.sdk.metrics import Histogram, MeterProvider
2223
from opentelemetry.sdk.metrics.export import (
2324
AggregationTemporality,
@@ -46,6 +47,9 @@ def test_synchronous_delta_temporality(self):
4647
)
4748

4849
provider = MeterProvider(metric_readers=[reader])
50+
# Disable SDK metrics
51+
# pylint: disable=protected-access
52+
reader._set_meter_provider(NoOpMeterProvider())
4953
meter = provider.get_meter("name", "version")
5054

5155
histogram = meter.create_histogram("histogram")
@@ -159,7 +163,7 @@ def test_synchronous_delta_temporality(self):
159163
provider.shutdown()
160164

161165
@mark.skipif(
162-
system() != "Linux",
166+
system() == "Windows",
163167
reason=(
164168
"Tests fail because Windows time_ns resolution is too low so "
165169
"two different time measurements may end up having the exact same"
@@ -177,6 +181,9 @@ def test_synchronous_cumulative_temporality(self):
177181
)
178182

179183
provider = MeterProvider(metric_readers=[reader])
184+
# Disable SDK metrics
185+
# pylint: disable=protected-access
186+
reader._set_meter_provider(NoOpMeterProvider())
180187
meter = provider.get_meter("name", "version")
181188

182189
histogram = meter.create_histogram("histogram")

opentelemetry-sdk/tests/metrics/integration_test/test_exponential_bucket_histogram.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from pytest import mark
2020

21+
from opentelemetry.metrics import NoOpMeterProvider
2122
from opentelemetry.sdk.metrics import Histogram, MeterProvider
2223
from opentelemetry.sdk.metrics.export import (
2324
AggregationTemporality,
@@ -57,6 +58,9 @@ def test_synchronous_delta_temporality(self):
5758
)
5859

5960
provider = MeterProvider(metric_readers=[reader])
61+
# Disable SDK metrics
62+
# pylint: disable=protected-access
63+
reader._set_meter_provider(NoOpMeterProvider())
6064
meter = provider.get_meter("name", "version")
6165

6266
histogram = meter.create_histogram("histogram")
@@ -191,6 +195,9 @@ def test_synchronous_cumulative_temporality(self):
191195
)
192196

193197
provider = MeterProvider(metric_readers=[reader])
198+
# Disable SDK metrics
199+
# pylint: disable=protected-access
200+
reader._set_meter_provider(NoOpMeterProvider())
194201
meter = provider.get_meter("name", "version")
195202

196203
histogram = meter.create_histogram("histogram")

opentelemetry-sdk/tests/metrics/integration_test/test_sum_aggregation.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from pytest import mark
2222

2323
from opentelemetry.context import Context
24-
from opentelemetry.metrics import Observation
24+
from opentelemetry.metrics import NoOpMeterProvider, Observation
2525
from opentelemetry.sdk.metrics import Counter, MeterProvider, ObservableCounter
2626
from opentelemetry.sdk.metrics._internal.exemplar import AlwaysOnExemplarFilter
2727
from opentelemetry.sdk.metrics.export import (
@@ -33,7 +33,7 @@
3333

3434
class TestSumAggregation(TestCase):
3535
@mark.skipif(
36-
system() != "Linux",
36+
system() == "Windows",
3737
reason=(
3838
"Tests fail because Windows time_ns resolution is too low so "
3939
"two different time measurements may end up having the exact same"
@@ -68,6 +68,9 @@ def observable_counter_callback(callback_options):
6868
)
6969

7070
provider = MeterProvider(metric_readers=[reader])
71+
# Disable SDK metrics
72+
# pylint: disable=protected-access
73+
reader._set_meter_provider(NoOpMeterProvider())
7174
meter = provider.get_meter("name", "version")
7275

7376
meter.create_observable_counter(
@@ -156,7 +159,7 @@ def observable_counter_callback(callback_options):
156159
self.assertIsNone(metrics_data)
157160

158161
@mark.skipif(
159-
system() != "Linux",
162+
system() == "Windows",
160163
reason=(
161164
"Tests fail because Windows time_ns resolution is too low so "
162165
"two different time measurements may end up having the exact same"
@@ -191,6 +194,9 @@ def observable_counter_callback(callback_options):
191194
)
192195

193196
provider = MeterProvider(metric_readers=[reader])
197+
# Disable SDK metrics
198+
# pylint: disable=protected-access
199+
reader._set_meter_provider(NoOpMeterProvider())
194200
meter = provider.get_meter("name", "version")
195201

196202
meter.create_observable_counter(
@@ -251,7 +257,7 @@ def observable_counter_callback(callback_options):
251257
self.assertIsNone(metrics_data)
252258

253259
@mark.skipif(
254-
system() != "Linux",
260+
system() == "Windows",
255261
reason=(
256262
"Tests fail because Windows time_ns resolution is too low so "
257263
"two different time measurements may end up having the exact same"
@@ -267,6 +273,9 @@ def test_synchronous_delta_temporality(self):
267273
)
268274

269275
provider = MeterProvider(metric_readers=[reader])
276+
# Disable SDK metrics
277+
# pylint: disable=protected-access
278+
reader._set_meter_provider(NoOpMeterProvider())
270279
meter = provider.get_meter("name", "version")
271280

272281
counter = meter.create_counter("counter")
@@ -378,7 +387,7 @@ def test_synchronous_delta_temporality(self):
378387
provider.shutdown()
379388

380389
@mark.skipif(
381-
system() != "Linux",
390+
system() == "Windows",
382391
reason=(
383392
"Tests fail because Windows time_ns resolution is too low so "
384393
"two different time measurements may end up having the exact same"
@@ -394,6 +403,9 @@ def test_synchronous_cumulative_temporality(self):
394403
)
395404

396405
provider = MeterProvider(metric_readers=[reader])
406+
# Disable SDK metrics
407+
# pylint: disable=protected-access
408+
reader._set_meter_provider(NoOpMeterProvider())
397409
meter = provider.get_meter("name", "version")
398410

399411
counter = meter.create_counter("counter")

0 commit comments

Comments
 (0)